From 362c124e8fe6274c7f543c8ad8a43fe46de8c0f9 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 17 Oct 2021 17:44:07 +0400 Subject: [PATCH 001/110] Allow to place levelled creatures to the game world (bug 6347) --- CHANGELOG.md | 2 +- apps/openmw/mwclass/creaturelevlist.cpp | 19 +++++++++++++++++++ apps/openmw/mwclass/creaturelevlist.hpp | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ae0c45d0..d3c70897cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Bug #6322: Total sold/cost should reset to 0 when there are no items offered Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house Bug #6326: Detect Enchantment/Key should detect items in unresolved containers + Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures Feature #890: OpenMW-CS: Column filtering Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console @@ -79,7 +80,6 @@ Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings Task #6264: Remove the old classes in animation.cpp - 0.47.0 ------ diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index e8023ce26b..ddb5fd2b38 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -5,6 +5,7 @@ #include "../mwmechanics/levelledlist.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -27,6 +28,24 @@ namespace MWClass } }; + MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + { + const MWWorld::LiveCellRef *ref = ptr.get(); + + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const + { + if (ptr.getRefData().getCustomData() == nullptr) + return; + + CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); + MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (!creature.isEmpty()) + MWBase::Environment::get().getWorld()->adjustPosition(creature, force); + } + std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 35152a9422..b3a940682c 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -32,6 +32,10 @@ namespace MWClass ///< Write additional state from \a ptr into \a state. void respawn (const MWWorld::Ptr& ptr) const override; + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + + void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } From dd4d8b26494ee1dd5bef2f3669f299f5a549bcb3 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 19 Oct 2021 21:38:07 +0200 Subject: [PATCH 002/110] Prevent floating point issues accumulating --- apps/openmw/mwmechanics/magiceffects.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index e75361628b..8e74d73d5d 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -1,10 +1,20 @@ #include "magiceffects.hpp" +#include #include #include #include +namespace +{ + // Round value to prevent precision issues + void truncate(float& value) + { + value = std::roundf(value * 1000.f) / 1000.f; + } +} + namespace MWMechanics { EffectKey::EffectKey() : mId (0), mArg (-1) {} @@ -74,6 +84,7 @@ namespace MWMechanics { mModifier += param.mModifier; mBase += param.mBase; + truncate(mModifier); return *this; } @@ -81,6 +92,7 @@ namespace MWMechanics { mModifier -= param.mModifier; mBase -= param.mBase; + truncate(mModifier); return *this; } From 07d505563e323a424f2f86c017c377d0c52ea9a2 Mon Sep 17 00:00:00 2001 From: Fanael Linithien Date: Fri, 22 Oct 2021 23:33:10 +0000 Subject: [PATCH 003/110] Remove non-existent file from CMakeLists --- components/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7b06b6702d..7e1abe0a4e 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -55,7 +55,7 @@ add_component_dir (shader add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller - lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer + lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt screencapture ) From e23b0edda4a49296c56b29251fe3f00fd6b0adba Mon Sep 17 00:00:00 2001 From: wareya Date: Fri, 22 Oct 2021 23:11:23 -0400 Subject: [PATCH 004/110] Add pre-pass to collision trace function that traces over less distance if possible --- apps/openmw/mwphysics/trace.cpp | 74 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 049d026e8e..b2c5410a62 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -12,31 +12,49 @@ namespace MWPhysics { -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& from, const btVector3& to, const btCollisionWorld* world, bool actorFilter) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - const btTransform &trans = actor->getWorldTransform(); - btTransform from(trans); - btTransform to(trans); - from.setOrigin(btstart); - to.setOrigin(btend); + btTransform transFrom(trans); + btTransform transTo(trans); + transFrom.setOrigin(from); + transTo.setOrigin(to); - const btVector3 motion = btstart-btend; + const btCollisionShape *shape = actor->getCollisionShape(); + assert(shape->isConvex()); + + const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + if(actorFilter) + newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - const btCollisionShape *shape = actor->getCollisionShape(); - assert(shape->isConvex()); - world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); + world->convexSweepTest(static_cast(shape), transFrom, transTo, newTraceCallback); + return newTraceCallback; +} + +void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +{ + const btVector3 btstart = Misc::Convert::toBullet(start); + btVector3 btend = Misc::Convert::toBullet(end); + + bool do_fallback = false; + if((btend-btstart).length2() > 5.0*5.0) + { + btend = btstart + (btend-btstart).normalized()*5.0; + do_fallback = true; + } + + auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: if(newTraceCallback.hasHit()) { mFraction = newTraceCallback.m_closestHitFraction; + if((end-start).length2() > 0.0) + mFraction *= (btend-btstart).length() / (end-start).length(); mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); @@ -44,6 +62,22 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star } else { + if(do_fallback) + { + btend = Misc::Convert::toBullet(end); + auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + + if(newTraceCallback.hasHit()) + { + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mEndPos = (end-start)*mFraction + start; + mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); + mHitObject = newTraceCallback.m_hitCollisionObject; + return; + } + } + // fallthrough mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; @@ -54,21 +88,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - - const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); - btTransform from(trans.getBasis(), btstart); - btTransform to(trans.getBasis(), btend); - - const btVector3 motion = btstart-btend; - ActorConvexCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - - world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); + auto newTraceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); if(newTraceCallback.hasHit()) { mFraction = newTraceCallback.m_closestHitFraction; From b5c876299d4f7d7cefa57caba57bec28f8719670 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 23 Oct 2021 09:29:16 +0200 Subject: [PATCH 005/110] Round to a power of two --- apps/openmw/mwmechanics/magiceffects.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 8e74d73d5d..b01c5446a7 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -11,7 +11,7 @@ namespace // Round value to prevent precision issues void truncate(float& value) { - value = std::roundf(value * 1000.f) / 1000.f; + value = std::roundf(value * 1024.f) / 1024.f; } } From c9c8d02332ff334eddf20eeaf1670dbf0b15c861 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 23 Oct 2021 08:31:46 +0000 Subject: [PATCH 006/110] fixes a crash (#3183) This PR fixes a crash caused by the improperly ensured lifetime of RigGeometry::mSourceGeometry. mSourceGeometry was not adequate to ensure mSourceGeometry would outlive mGeometry because we extend mGeometrys lifetime beyond this lifetime by passing mGeometry to the draw traversal instead of this. In addition, We add important comments. We detect and prevent generally unsafe operations in high level code. We add a sprinkling of const to help clarify intentions. --- apps/opencs/view/render/actor.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 4 +-- apps/openmw/mwrender/creatureanimation.cpp | 2 +- apps/openmw/mwrender/npcanimation.cpp | 2 +- apps/openmw/mwrender/objectpaging.cpp | 7 ++++- components/resource/scenemanager.cpp | 31 ++++++++++++++-------- components/resource/scenemanager.hpp | 14 +++++++--- components/sceneutil/attach.cpp | 22 ++++++++------- components/sceneutil/attach.hpp | 6 ++++- components/sceneutil/clone.hpp | 1 + components/sceneutil/morphgeometry.cpp | 9 +++++++ components/sceneutil/riggeometry.cpp | 11 ++++++++ components/sceneutil/riggeometry.hpp | 7 +++++ 13 files changed, 86 insertions(+), 32 deletions(-) diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index 271ca2365a..94b82c96cd 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -111,7 +111,7 @@ namespace CSVRender if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(mesh); - SceneUtil::attach(instance, mSkeleton, boneName, node->second); + SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 10624c8bca..ca76856d48 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1319,10 +1319,10 @@ namespace MWRender cache.insert(std::make_pair(model, created)); - return sceneMgr->createInstance(created); + return sceneMgr->getInstance(created); } else - return sceneMgr->createInstance(found->second); + return sceneMgr->getInstance(found->second); } else { diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 502340d4b9..b64b205961 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -164,7 +164,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); + osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get(), mResourceSystem->getSceneManager()); scene.reset(new PartHolder(attached)); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 0e100326dd..42215c00bc 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -717,7 +717,7 @@ PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const st if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second); + osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 5c22c3fc8d..947f23f781 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -617,6 +617,10 @@ namespace MWRender pat->setAttitude(nodeAttitude); } + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateMultiRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by needvbo() in optimizer.cpp) copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); copyop.mOptimizeBillboards = (size > 1/4.f); copyop.mNodePath.push_back(trans); @@ -645,7 +649,8 @@ namespace MWRender } if (numinstances > 0) { - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, we hint to the cache that it's still being used and should be kept in cache templateRefs->addRef(cnode); if (pair.second.mNeedCompile) diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index c5ef957c3e..4184a77c54 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -693,19 +693,33 @@ namespace Resource } } - osg::ref_ptr SceneManager::createInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); - return createInstance(scene); + return getInstance(scene); } - osg::ref_ptr SceneManager::createInstance(const osg::Node *base) + osg::ref_ptr SceneManager::cloneNode(const osg::Node* base) { - osg::ref_ptr cloned = static_cast(base->clone(SceneUtil::CopyOp())); - - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + SceneUtil::CopyOp copyop; + if (const osg::Drawable* drawable = base->asDrawable()) + { + if (drawable->asGeometry()) + { + Log(Debug::Warning) << "SceneManager::cloneNode: attempting to clone osg::Geometry. For safety reasons this will be expensive. Consider avoiding this call."; + copyop.setCopyFlags(copyop.getCopyFlags()|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES); + } + } + osg::ref_ptr cloned = static_cast(base->clone(copyop)); + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, if this node is managed by a cache, we hint to the cache that it's still being used and should be kept in cache cloned->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(base)); + return cloned; + } + osg::ref_ptr SceneManager::getInstance(const osg::Node *base) + { + osg::ref_ptr cloned = cloneNode(base); // we can skip any scene graphs without update callbacks since we know that particle emitters will have an update callback set if (cloned->getNumChildrenRequiringUpdateTraversal() > 0) { @@ -716,11 +730,6 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string &name) - { - return createInstance(name); - } - osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index b5d3e453a0..85e012071d 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -132,16 +132,22 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - osg::ref_ptr createInstance(const std::string& name); + /// Clone osg::Node safely. + /// @note Thread safe. + static osg::ref_ptr cloneNode(const osg::Node* base); - osg::ref_ptr createInstance(const osg::Node* base); void shareState(osg::ref_ptr node); - /// Get an instance of the given scene template + + /// Clone osg::Node and adjust it according to SceneManager's settings. + /// @note Thread safe. + osg::ref_ptr getInstance(const osg::Node* base); + + /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. osg::ref_ptr getInstance(const std::string& name); - /// Get an instance of the given scene template and immediately attach it to a parent node + /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 6690148c74..9dabb282b0 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -13,9 +13,9 @@ #include #include +#include #include "visitor.hpp" -#include "clone.hpp" namespace SceneUtil { @@ -49,10 +49,10 @@ namespace SceneUtil if (!filterMatches(drawable.getName())) return; - osg::Node* node = &drawable; + const osg::Node* node = &drawable; for (auto it = getNodePath().rbegin()+1; it != getNodePath().rend(); ++it) { - osg::Node* parent = *it; + const osg::Node* parent = *it; if (!filterMatches(parent->getName())) break; node = parent; @@ -60,11 +60,11 @@ namespace SceneUtil mToCopy.emplace(node); } - void doCopy() + void doCopy(Resource::SceneManager* sceneManager) { - for (const osg::ref_ptr& node : mToCopy) + for (const osg::ref_ptr& node : mToCopy) { - mParent->addChild(static_cast(node->clone(SceneUtil::CopyOp()))); + mParent->addChild(sceneManager->getInstance(node)); } mToCopy.clear(); } @@ -78,7 +78,7 @@ namespace SceneUtil || (lowerName.size() >= mFilter2.size() && lowerName.compare(0, mFilter2.size(), mFilter2) == 0); } - using NodeSet = std::set>; + using NodeSet = std::set>; NodeSet mToCopy; osg::ref_ptr mParent; @@ -100,7 +100,7 @@ namespace SceneUtil } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager) { if (dynamic_cast(toAttach.get())) { @@ -108,7 +108,9 @@ namespace SceneUtil CopyRigVisitor copyVisitor(handle, filter); const_cast(toAttach.get())->accept(copyVisitor); - copyVisitor.doCopy(); + copyVisitor.doCopy(sceneManager); + // add a ref to the original template to hint to the cache that it is still being used and should be kept in cache. + handle->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(toAttach)); if (handle->getNumChildren() == 1) { @@ -127,7 +129,7 @@ namespace SceneUtil } else { - osg::ref_ptr clonedToAttach = static_cast(toAttach->clone(SceneUtil::CopyOp())); + osg::ref_ptr clonedToAttach = sceneManager->getInstance(toAttach); FindByNameVisitor findBoneOffset("BoneOffset"); clonedToAttach->accept(findBoneOffset); diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index 806fc53488..f5fc693724 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -10,6 +10,10 @@ namespace osg class Node; class Group; } +namespace Resource +{ + class SceneManager; +} namespace SceneUtil { @@ -19,7 +23,7 @@ namespace SceneUtil /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager); } diff --git a/components/sceneutil/clone.hpp b/components/sceneutil/clone.hpp index 1cf00c9e58..35240cbfba 100644 --- a/components/sceneutil/clone.hpp +++ b/components/sceneutil/clone.hpp @@ -18,6 +18,7 @@ namespace SceneUtil /// @par Defines the cloning behaviour we need: /// * Assigns updated ParticleSystem pointers on cloned emitters and programs. /// * Deep copies RigGeometry and MorphGeometry so they can animate without affecting clones. + /// @warning Avoid using this class directly. The safety of cloning operations depends on the copy flags and the objects involved. Consider using SceneManager::cloneNode for additional safety. /// @warning Do not use an object of this class for more than one copy operation. class CopyOp : public osg::CopyOp { diff --git a/components/sceneutil/morphgeometry.cpp b/components/sceneutil/morphgeometry.cpp index 78be559989..59adbffffe 100644 --- a/components/sceneutil/morphgeometry.cpp +++ b/components/sceneutil/morphgeometry.cpp @@ -1,6 +1,7 @@ #include "morphgeometry.hpp" #include +#include #include @@ -27,11 +28,19 @@ MorphGeometry::MorphGeometry(const MorphGeometry ©, const osg::CopyOp ©o void MorphGeometry::setSourceGeometry(osg::ref_ptr sourceGeom) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeom; for (unsigned int i=0; i<2; ++i) { + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(*mSourceGeometry, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); const osg::Geometry& from = *mSourceGeometry; osg::Geometry& to = *mGeometry[i]; diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index ec00efa535..84b31f4afc 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "skeleton.hpp" @@ -60,12 +61,22 @@ RigGeometry::RigGeometry(const RigGeometry ©, const osg::CopyOp ©op) void RigGeometry::setSourceGeometry(osg::ref_ptr sourceGeometry) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeometry; for (unsigned int i=0; i<2; ++i) { const osg::Geometry& from = *sourceGeometry; + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by mSourceGeometry) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(from, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); + osg::Geometry& to = *mGeometry[i]; to.setSupportsDisplayList(false); to.setUseVertexBufferObjects(true); diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index e01583399e..25ae5a3243 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -9,6 +9,13 @@ namespace SceneUtil class Skeleton; class Bone; + // TODO: This class has a lot of issues. + // - We require too many workarounds to ensure safety. + // - mSourceGeometry should be const, but can not be const because of a use case in shadervisitor.cpp. + // - We create useless mGeometry clones in template RigGeometries. + // - We do not support compileGLObjects. + // - We duplicate some code in MorphGeometry. + /// @brief Mesh skinning implementation. /// @note A RigGeometry may be attached directly to a Skeleton, or somewhere below a Skeleton. /// Note though that the RigGeometry ignores any transforms below the Skeleton, so the attachment point is not that important. From e65af0bf0678c925efafe0987608fa56d45893c9 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sat, 23 Oct 2021 10:48:37 +0200 Subject: [PATCH 007/110] Silence all opengl deprecation warnings for MacOS We know... --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0451b639e5..1ae3841bdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,6 +707,7 @@ endif() if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") + set_target_properties(components PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") endif() # Apple bundling From dfb6bdf77ec3cc40126d37b9728160a22fd1bbe4 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 13 Oct 2021 22:44:18 +0200 Subject: [PATCH 008/110] Allow integer variable names --- components/compiler/declarationparser.cpp | 10 ++++++++++ components/compiler/declarationparser.hpp | 4 ++++ components/compiler/lineparser.cpp | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/components/compiler/declarationparser.cpp b/components/compiler/declarationparser.cpp index 1c64aaadee..f29fe820c1 100644 --- a/components/compiler/declarationparser.cpp +++ b/components/compiler/declarationparser.cpp @@ -90,6 +90,16 @@ bool Compiler::DeclarationParser::parseSpecial (int code, const TokenLoc& loc, S return Parser::parseSpecial (code, loc, scanner); } +bool Compiler::DeclarationParser::parseInt(int value, const TokenLoc& loc, Scanner& scanner) +{ + if(mState == State_Name) + { + // Allow integers to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } + return Parser::parseInt(value, loc, scanner); +} + void Compiler::DeclarationParser::reset() { mState = State_Begin; diff --git a/components/compiler/declarationparser.hpp b/components/compiler/declarationparser.hpp index c04f1dc268..d08509fe50 100644 --- a/components/compiler/declarationparser.hpp +++ b/components/compiler/declarationparser.hpp @@ -35,6 +35,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void reset() override; }; diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 77afaee8bd..943c052473 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -67,6 +67,11 @@ namespace Compiler parseExpression (scanner, loc); return true; } + else if (mState == SetState) + { + // Allow ints to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } return Parser::parseInt (value, loc, scanner); } From 31aa19574b31a9bad3efd686ffd909ad976554e6 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 21 Oct 2021 16:54:32 +0200 Subject: [PATCH 009/110] Make PositionCell take additional junk arguments --- components/compiler/extensions0.cpp | 2 +- components/compiler/stringparser.cpp | 6 ++++++ components/compiler/stringparser.hpp | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 3dfcadab10..64133bee84 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -553,7 +553,7 @@ namespace Compiler extensions.registerFunction("getpos",'f',"c",opcodeGetPos,opcodeGetPosExplicit); extensions.registerFunction("getstartingpos",'f',"c",opcodeGetStartingPos,opcodeGetStartingPosExplicit); extensions.registerInstruction("position","ffffz",opcodePosition,opcodePositionExplicit); - extensions.registerInstruction("positioncell","ffffcX",opcodePositionCell,opcodePositionCellExplicit); + extensions.registerInstruction("positioncell","ffffczz",opcodePositionCell,opcodePositionCellExplicit); extensions.registerInstruction("placeitemcell","ccffffX",opcodePlaceItemCell); extensions.registerInstruction("placeitem","cffffX",opcodePlaceItem); extensions.registerInstruction("placeatpc","clflX",opcodePlaceAtPc); diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index d9c3c04947..4e0114e0a1 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -86,6 +86,12 @@ namespace Compiler return Parser::parseSpecial (code, loc, scanner); } + bool StringParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) + { + reportWarning("Treating integer argument as a string", loc); + return parseName(loc.mLiteral, loc, scanner); + } + void StringParser::append (std::vector& code) { std::copy (mCode.begin(), mCode.end(), std::back_inserter (code)); diff --git a/components/compiler/stringparser.hpp b/components/compiler/stringparser.hpp index 1976628360..07b61d8fda 100644 --- a/components/compiler/stringparser.hpp +++ b/components/compiler/stringparser.hpp @@ -43,6 +43,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void append (std::vector& code); ///< Append code for parsed string. From 41318a585fa19e34da86dff1b8fd5354d5163db1 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 23 Oct 2021 10:40:26 +0000 Subject: [PATCH 010/110] fixes enable and disable commands (#3186) This PR fixes a recent regression concerning enable and disable commands with object paging. In addition, we add a necessary comment to avoid such issues in the future. --- components/terrain/viewdata.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index 3ebc99f1df..e517390b44 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -74,6 +74,8 @@ const osg::Vec3f& ViewData::getViewPoint() const return mViewPoint; } +// NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here. +// If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset(). void ViewData::reset() { // clear any unused entries @@ -164,9 +166,13 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo } else if (!mostSuitableView) { + if (vd->getWorldUpdateRevision() != mWorldUpdateRevision) + { + vd->setWorldUpdateRevision(mWorldUpdateRevision); + vd->clear(); + } vd->setViewPoint(viewPoint); vd->setActiveGrid(activeGrid); - vd->setWorldUpdateRevision(mWorldUpdateRevision); needsUpdate = true; } } From 62b59a3c001d43c5468b1e17b8a40fe8015d40d1 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sat, 23 Oct 2021 12:56:02 +0200 Subject: [PATCH 011/110] Update CMakeLists.txt --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ae3841bdd..537c90670d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -708,6 +708,7 @@ if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") set_target_properties(components PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") + set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") endif() # Apple bundling From 1e40d27318de5aed3f1b148c441ed9ba44e6f6f1 Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Sat, 23 Oct 2021 17:53:38 -0700 Subject: [PATCH 012/110] introduce sky shaders --- CHANGELOG.md | 4 + apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwrender/sky.cpp | 2430 ++++++++------------------- apps/openmw/mwrender/sky.hpp | 95 +- apps/openmw/mwrender/skyutil.cpp | 1142 +++++++++++++ apps/openmw/mwrender/skyutil.hpp | 343 ++++ apps/openmw/mwworld/weather.cpp | 2243 ++++++++++++------------ apps/openmw/mwworld/weather.hpp | 4 + components/shader/shadervisitor.cpp | 4 + files/shaders/CMakeLists.txt | 3 + files/shaders/lighting.glsl | 10 + files/shaders/objects_fragment.glsl | 8 +- files/shaders/objects_vertex.glsl | 8 +- files/shaders/sky_fragment.glsl | 84 + files/shaders/sky_vertex.glsl | 20 + files/shaders/skypasses.glsl | 7 + 16 files changed, 3453 insertions(+), 2954 deletions(-) create mode 100644 apps/openmw/mwrender/skyutil.cpp create mode 100644 apps/openmw/mwrender/skyutil.hpp create mode 100644 files/shaders/sky_fragment.glsl create mode 100644 files/shaders/sky_vertex.glsl create mode 100644 files/shaders/skypasses.glsl diff --git a/CHANGELOG.md b/CHANGELOG.md index b37fed31fd..188238f42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4700: Editor: Incorrect command implementation Bug #4744: Invisible particles must still be processed + Bug #4752: UpdateCellCommand doesn't undo properly + Bug #5088: Sky abruptly changes direction during certain weather transitions Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system Bug #5207: Loose summons can be present in scene @@ -40,6 +42,7 @@ Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6168: Weather particles flicker for a frame at start of storms Bug #6172: Some creatures can't open doors Bug #6174: Spellmaking and Enchanting sliders differences from vanilla Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla @@ -73,6 +76,7 @@ Feature #6017: Separate persistent and temporary cell references when saving Feature #6032: Reverse-z depth buffer Feature #6078: First person should not clear depth buffer + Feature #6161: Refactor Sky to use shaders and GLES/GL3 friendly Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly Feature #6199: Support FBO Rendering Feature #6249: Alpha testing support for Collada diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 7334d893db..425d859f31 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -19,7 +19,7 @@ set(GAME_HEADER source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index ebf901b5ab..217f10f294 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,1306 +1,185 @@ #include "sky.hpp" -#include - -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include #include +#include +#include #include +#include -#include +#include -#include +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include - -#include +#include +#include #include +#include "../mwworld/weather.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "vismask.hpp" #include "renderbin.hpp" #include "util.hpp" +#include "skyutil.hpp" namespace { - osg::ref_ptr createAlphaTrackingUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); - return mat; - } - - osg::ref_ptr createUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; - } - - osg::ref_ptr createTexturedQuad(int numUvSets=1, float scale=1.f) - { - osg::ref_ptr geom = new osg::Geometry; - - osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5*scale, -0.5*scale, 0)); - verts->push_back(osg::Vec3f(-0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, -0.5*scale, 0)); - - geom->setVertexArray(verts); - - osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(0, 0)); - texcoords->push_back(osg::Vec2f(0, 1)); - texcoords->push_back(osg::Vec2f(1, 1)); - texcoords->push_back(osg::Vec2f(1, 0)); - - osg::ref_ptr colors = new osg::Vec4Array; - colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); - geom->setColorArray(colors, osg::Array::BIND_OVERALL); - - for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); - - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); - - return geom; - } - -} - -namespace MWRender -{ - -class AtmosphereUpdater : public SceneUtil::StateSetUpdater -{ -public: - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - } - -private: - osg::Vec4f mEmissionColor; -}; - -class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater -{ -public: - AtmosphereNightUpdater(Resource::ImageManager* imageManager) - { - // we just need a texture, its contents don't really matter - mTexture = new osg::Texture2D(imageManager->getWarningImage()); - } - - void setFade(const float fade) - { - mColor.a() = fade; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr texEnv (new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - - stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mColor); - } - - osg::ref_ptr mTexture; - - osg::Vec4f mColor; -}; - -class CloudUpdater : public SceneUtil::StateSetUpdater -{ -public: - CloudUpdater() - : mAnimationTimer(0.f) - , mOpacity(0.f) - { - } - - void setAnimationTimer(float timer) - { - mAnimationTimer = timer; - } - - void setTexture(osg::ref_ptr texture) - { - mTexture = texture; - } - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - void setOpacity(float opacity) - { - mOpacity = opacity; - } - -protected: - void setDefaults(osg::StateSet *stateset) override - { - osg::ref_ptr texmat (new osg::TexMat); - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); - stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already - osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); - texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); - texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); - - stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override - { - osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); - - stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - - osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); - } - -private: - float mAnimationTimer; - osg::ref_ptr mTexture; - osg::Vec4f mEmissionColor; - float mOpacity; -}; - -/// Transform that removes the eyepoint of the modelview matrix, -/// i.e. its children are positioned relative to the camera. -class CameraRelativeTransform : public osg::Transform -{ -public: - CameraRelativeTransform() - { - // Culling works in node-local space, not in camera space, so we can't cull this node correctly - // That's not a problem though, children of this node can be culled just fine - // Just make sure you do not place a CameraRelativeTransform deep in the scene graph - setCullingActive(false); - - addCullCallback(new CullCallback); - } - - CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) - : osg::Transform(copy, copyop) - { - } - - META_Node(MWRender, CameraRelativeTransform) - - const osg::Vec3f& getLastViewPoint() const - { - return mViewPoint; - } - - bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override - { - if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - mViewPoint = static_cast(nv)->getViewPoint(); - } - - if (_referenceFrame==RELATIVE_RF) - { - matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); - return false; - } - else // absolute - { - matrix.makeIdentity(); - return true; - } - } - - osg::BoundingSphere computeBound() const override - { - return osg::BoundingSphere(); - } - - class CullCallback : public SceneUtil::NodeCallback + class WrapAroundOperator : public osgParticle::Operator { public: - void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange) + : osgParticle::Operator() + , mCamera(camera) + , mWrapRange(wrapRange) + , mHalfWrapRange(mWrapRange / 2.0) { - // XXX have to remove unwanted culling plane of the water reflection camera + mPreviousCameraPosition = getCameraPosition(); + } - // Remove all planes that aren't from the standard frustum - unsigned int numPlanes = 4; - if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) - ++numPlanes; - if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) - ++numPlanes; + osg::Object *cloneType() const override + { + return nullptr; + } - unsigned int mask = 0x1; - unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); - for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + osg::Object *clone(const osg::CopyOp &op) const override + { + return nullptr; + } + + void operate(osgParticle::Particle *P, double dt) override + { + } + + void operateParticles(osgParticle::ParticleSystem *ps, double dt) override + { + osg::Vec3 position = getCameraPosition(); + osg::Vec3 positionDifference = position - mPreviousCameraPosition; + + osg::Matrix toWorld, toLocal; + + std::vector worldMatrices = ps->getWorldMatrices(); + + + if (!worldMatrices.empty()) { - if (i >= numPlanes) + toWorld = worldMatrices[0]; + toLocal.invert(toWorld); + } + + for (int i = 0; i < ps->numParticles(); ++i) + { + osgParticle::Particle *p = ps->getParticle(i); + p->setPosition(toWorld.preMult(p->getPosition())); + p->setPosition(p->getPosition() - positionDifference); + + for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions { - // turn off this culling plane - resultMask &= (~mask); + osg::Vec3 pos = p->getPosition(); + + if (pos[j] < -mHalfWrapRange[j]) + pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); + else if (pos[j] > mHalfWrapRange[j]) + pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; + + p->setPosition(pos); } - mask <<= 1; + p->setPosition(toLocal.preMult(p->getPosition())); } - cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); - cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); - - cv->getProjectionCullingStack().back().pushCurrentMask(); - cv->getCurrentCullingSet().pushCurrentMask(); - - traverse(node, cv); - - cv->getProjectionCullingStack().back().popCurrentMask(); - cv->getCurrentCullingSet().popCurrentMask(); - } - }; -private: - // viewPoint for the current frame - mutable osg::Vec3f mViewPoint; -}; - -class ModVertexAlphaVisitor : public osg::NodeVisitor -{ -public: - ModVertexAlphaVisitor(int meshType) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mMeshType(meshType) - { - } - - void apply(osg::Drawable& drw) override - { - osg::Geometry* geom = drw.asGeometry(); - if (!geom) - return; - - osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); - for (unsigned int i=0; isize(); ++i) - { - float alpha = 1.f; - if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (mMeshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 0.25098; // second row - else alpha = 1.f; - } - else if (mMeshType == 2) - { - if (geom->getColorArray()) - { - osg::Vec4Array* origColors = static_cast(geom->getColorArray()); - alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; - } - else - alpha = 1.f; - } - - (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); - } - - geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - } - -private: - int mMeshType; -}; - -/// @brief Hides the node subgraph if the eye point is below water. -/// @note Must be added as cull callback. -/// @note Meant to be used on a node that is child of a CameraRelativeTransform. -/// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public SceneUtil::NodeCallback -{ -public: - UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) - : mCameraRelativeTransform(cameraRelativeTransform) - , mEnabled(true) - , mWaterLevel(0.f) - { - } - - bool isUnderwater() - { - osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); - return mEnabled && viewPoint.z() < mWaterLevel; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) - { - if (isUnderwater()) - return; - - traverse(node, nv); - } - - void setEnabled(bool enabled) - { - mEnabled = enabled; - } - void setWaterLevel(float waterLevel) - { - mWaterLevel = waterLevel; - } - -private: - osg::ref_ptr mCameraRelativeTransform; - bool mEnabled; - float mWaterLevel; -}; - -/// A base class for the sun and moons. -class CelestialBody -{ -public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) - : mVisibleMask(visibleMask) - { - mGeom = createTexturedQuad(numUvSets); - mTransform = new osg::PositionAttitudeTransform; - mTransform->setNodeMask(mVisibleMask); - mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); - mTransform->addChild(mGeom); - - parentNode->addChild(mTransform); - } - - virtual ~CelestialBody() {} - - virtual void adjustTransparency(const float ratio) = 0; - - void setVisible(bool visible) - { - mTransform->setNodeMask(visible ? mVisibleMask : 0); - } - -protected: - unsigned int mVisibleMask; - static const float mDistance; - osg::ref_ptr mTransform; - osg::ref_ptr mGeom; -}; - -const float CelestialBody::mDistance = 1000.0f; - -class Sun : public CelestialBody -{ -public: - Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) - : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) - , mUpdater(new Updater) - { - mTransform->addUpdateCallback(mUpdater); - - osg::ref_ptr sunTex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"))); - sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - - osg::ref_ptr queryNode (new osg::Group); - // Need to render after the world geometry so we can correctly test for occlusions - osg::StateSet* stateset = queryNode->getOrCreateStateSet(); - stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); - stateset->setNestRenderBins(false); - // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun - osg::ref_ptr alphaFunc (new osg::AlphaFunc); - alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - // Disable writing to the color buffer. We are using this geometry for visibility tests only. - osg::ref_ptr colormask (new osg::ColorMask(0, 0, 0, 0)); - stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); - - mTransform->addChild(queryNode); - - mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); - mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); - - createSunFlash(imageManager); - createSunGlare(); - } - - ~Sun() - { - mTransform->removeUpdateCallback(mUpdater); - destroySunFlash(); - destroySunGlare(); - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mColor.r() = color.r(); - mUpdater->mColor.g() = color.g(); - mUpdater->mColor.b() = color.b(); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mColor.a() = ratio; - if (mSunGlareCallback) - mSunGlareCallback->setGlareView(ratio); - if (mSunFlashCallback) - mSunFlashCallback->setGlareView(ratio); - } - - void setDirection(const osg::Vec3f& direction) - { - osg::Vec3f normalizedDirection = direction / direction.length(); - mTransform->setPosition(normalizedDirection * mDistance); - - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); - mTransform->setAttitude(quat); - } - - void setGlareTimeOfDayFade(float val) - { - if (mSunGlareCallback) - mSunGlareCallback->setTimeOfDayFade(val); - } - -private: - class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback - { - public: - osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } - }; - - /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. - osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) - { - osg::ref_ptr oqn = new osg::OcclusionQueryNode; - oqn->setQueriesEnabled(true); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced - osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); -#else - osg::ref_ptr queryGeom = oqn->getQueryGeometry(); -#endif - - // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, - // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry - // is only called once. - // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. - queryGeom->setDataVariance(osg::Object::STATIC); - - // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, - // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to - // circumvent this. - queryGeom->setVertexArray(mGeom->getVertexArray()); - queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); - queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); - queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); - - // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. - oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); - // Still need a proper bounding sphere. - oqn->setInitialBound(queryGeom->getBound()); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - oqn->setQueryGeometry(queryGeom.release()); -#endif - - osg::StateSet* queryStateSet = new osg::StateSet; - if (queryVisible) - { - auto depth = SceneUtil::createDepth(); - // This is a trick to make fragments written by the query always use the maximum depth value, - // without having to retrieve the current far clipping distance. - // We want the sun glare to be "infinitely" far away. - double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; - depth->setZNear(far); - depth->setZFar(far); - depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - else - { - queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - oqn->setQueryStateSet(queryStateSet); - - parent->addChild(oqn); - - return oqn; - } - - void createSunFlash(Resource::ImageManager& imageManager) - { - osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - osg::ref_ptr group (new osg::Group); - - mTransform->addChild(group); - - const float scale = 2.6f; - osg::ref_ptr geom = createTexturedQuad(1, scale); - group->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - - mSunFlashNode = group; - - mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); - mSunFlashNode->addCullCallback(mSunFlashCallback); - } - void destroySunFlash() - { - if (mSunFlashNode) - { - mSunFlashNode->removeCullCallback(mSunFlashCallback); - mSunFlashCallback = nullptr; - } - } - - void createSunGlare() - { - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? - camera->setViewMatrix(osg::Matrix::identity()); - camera->setClearMask(0); - camera->setRenderOrder(osg::Camera::NESTED_RENDER); - camera->setAllowEventFocus(false); - - osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); - - camera->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - - // set up additive blending - osg::ref_ptr blendFunc (new osg::BlendFunc); - blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); - blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); - mSunGlareNode = camera; - - mSunGlareNode->addCullCallback(mSunGlareCallback); - - mTransform->addChild(camera); - } - void destroySunGlare() - { - if (mSunGlareNode) - { - mSunGlareNode->removeCullCallback(mSunGlareCallback); - mSunGlareCallback = nullptr; - } - } - - class Updater : public SceneUtil::StateSetUpdater - { - public: - osg::Vec4f mColor; - - Updater() - : mColor(1.f, 1.f, 1.f, 1.f) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); - } - }; - - class OcclusionCallback - { - public: - OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : mOcclusionQueryVisiblePixels(oqnVisible) - , mOcclusionQueryTotalPixels(oqnTotal) - { + mPreviousCameraPosition = position; } protected: - float getVisibleRatio (osg::Camera* camera) + osg::Camera *mCamera; + osg::Vec3 mPreviousCameraPosition; + osg::Vec3 mWrapRange; + osg::Vec3 mHalfWrapRange; + + osg::Vec3 getCameraPosition() { - int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); - int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); - - float visibleRatio = 0.f; - if (total > 0) - visibleRatio = static_cast(visible) / static_cast(total); - - float dt = MWBase::Environment::get().getFrameDuration(); - - float lastRatio = mLastRatio[osg::observer_ptr(camera)]; - - float change = dt*10; - - if (visibleRatio > lastRatio) - visibleRatio = std::min(visibleRatio, lastRatio + change); - else - visibleRatio = std::max(visibleRatio, lastRatio - change); - - mLastRatio[osg::observer_ptr(camera)] = visibleRatio; - - return visibleRatio; + return mCamera->getInverseViewMatrix().getTrans(); } - - private: - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; - - std::map, float> mLastRatio; }; - /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + class WeatherAlphaOperator : public osgParticle::Operator { public: - SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : OcclusionCallback(oqnVisible, oqnTotal) - , mGlareView(1.f) + WeatherAlphaOperator(float& alpha, bool rain) + : mAlpha(alpha) + , mIsRain(rain) + { } + + osg::Object *cloneType() const override { + return nullptr; } - void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + osg::Object *clone(const osg::CopyOp &op) const override { - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - osg::ref_ptr stateset; - - if (visibleRatio > 0.f) - { - const float fadeThreshold = 0.1; - if (visibleRatio < fadeThreshold) - { - float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (createUnlitMaterial()); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); - stateset = new osg::StateSet; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - else if (visibleRatio < 1.f) - { - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; - } - } - - float scale = visibleRatio; - - if (scale == 0.f) - { - // no traverse - return; - } - else if (scale == 1.f) - traverse(node, cv); - else - { - osg::Matrix modelView = *cv->getModelViewMatrix(); - - modelView.preMultScale(osg::Vec3f(scale, scale, scale)); - - if (stateset) - cv->pushStateSet(stateset); - - cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - - traverse(node, cv); - - cv->popModelViewMatrix(); - - if (stateset) - cv->popStateSet(); - } + return nullptr; } - void setGlareView(float value) + void operate(osgParticle::Particle *particle, double dt) override { - mGlareView = value; + constexpr float rainThreshold = 0.6f; // Rain_Threshold? + float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; + particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } private: - float mGlareView; + float &mAlpha; + bool mIsRain; }; - - /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. - /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. + class AlphaFader : public SceneUtil::StateSetUpdater { public: - SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, - osg::ref_ptr sunTransform) - : OcclusionCallback(oqnVisible, oqnTotal) - , mSunTransform(sunTransform) - , mTimeOfDayFade(1.f) - , mGlareView(1.f) - { - mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); - mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); - mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); - - // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, - // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, - // so the resulting color looks more orange than red. - mColor *= 2; - for (int i=0; i<3; ++i) - mColor[i] = std::min(1.f, mColor[i]); - } - - void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) - { - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); - - float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); - float fade = value * mSunGlareFaderMax; - - fade *= mTimeOfDayFade * mGlareView * visibleRatio; - - if (fade == 0.f) - { - // no traverse - return; - } - else - { - osg::ref_ptr stateset (new osg::StateSet); - - osg::ref_ptr mat (createUnlitMaterial()); - - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); - - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - cv->pushStateSet(stateset); - traverse(node, cv); - cv->popStateSet(); - } - } - - void setTimeOfDayFade(float val) - { - mTimeOfDayFade = val; - } - - void setGlareView(float glareView) - { - mGlareView = glareView; - } - - private: - float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const - { - osg::Vec3d eye, center, up; - viewMatrix.getLookAt(eye, center, up); - - osg::Vec3d forward = center - eye; - osg::Vec3d sun = mSunTransform->getPosition(); - - forward.normalize(); - sun.normalize(); - float angleRadians = std::acos(forward * sun); - return angleRadians; - } - - osg::ref_ptr mSunTransform; - float mTimeOfDayFade; - float mGlareView; - osg::Vec4f mColor; - float mSunGlareFaderMax; - float mSunGlareFaderAngleMax; - }; - - osg::ref_ptr mUpdater; - osg::ref_ptr mSunFlashCallback; - osg::ref_ptr mSunFlashNode; - osg::ref_ptr mSunGlareCallback; - osg::ref_ptr mSunGlareNode; - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; -}; - -class Moon : public CelestialBody -{ -public: - enum Type - { - Type_Masser = 0, - Type_Secunda - }; - - Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) - : CelestialBody(parentNode, scaleFactor, 2) - , mType(type) - , mPhase(MoonState::Phase::Unspecified) - , mUpdater(new Updater(imageManager)) - { - setPhase(MoonState::Phase::Full); - setVisible(true); - - mGeom->addUpdateCallback(mUpdater); - } - - ~Moon() - { - mGeom->removeUpdateCallback(mUpdater); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mTransparency *= ratio; - } - - void setState(const MoonState& state) - { - float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; - float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; - - osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); - - osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); - mTransform->setPosition(direction * mDistance); - - // The moon quad is initially oriented facing down, so we need to offset its X-axis - // rotation to rotate it to face the camera when sitting at the horizon. - osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - mTransform->setAttitude(attX * rotZ); - - setPhase(state.mPhase); - mUpdater->mTransparency = state.mMoonAlpha; - mUpdater->mShadowBlend = state.mShadowBlend; - } - - void setAtmosphereColor(const osg::Vec4f& color) - { - mUpdater->mAtmosphereColor = color; - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mMoonColor = color; - } - - unsigned int getPhaseInt() const - { - if (mPhase == MoonState::Phase::New) return 0; - else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; - else if (mPhase == MoonState::Phase::WaningCrescent) return 1; - else if (mPhase == MoonState::Phase::FirstQuarter) return 2; - else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; - else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; - else if (mPhase == MoonState::Phase::WaningGibbous) return 3; - else if (mPhase == MoonState::Phase::Full) return 4; - return 0; - } - -private: - struct Updater : public SceneUtil::StateSetUpdater - { - Resource::ImageManager& mImageManager; - osg::ref_ptr mPhaseTex; - osg::ref_ptr mCircleTex; - float mTransparency; - float mShadowBlend; - osg::Vec4f mAtmosphereColor; - osg::Vec4f mMoonColor; - - Updater(Resource::ImageManager& imageManager) - : mImageManager(imageManager) - , mPhaseTex() - , mCircleTex() - , mTransparency(1.0f) - , mShadowBlend(1.0f) - , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) - , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) - { - } + /// @param alpha the variable alpha value is recovered from + AlphaFader(const float& alpha) + : mAlpha(alpha) + { } void setDefaults(osg::StateSet* stateset) override { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); - - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv2 = new osg::TexEnvCombine; - texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); - texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); - texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); - - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + // need to create a deep copy of StateAttributes we will modify + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mMoonColor * mShadowBlend); - - osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, mAlpha)); } - void setTextures(const std::string& phaseTex, const std::string& circleTex) - { - mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); - mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); - mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - reset(); - } + protected: + const float &mAlpha; }; - Type mType; - MoonState::Phase mPhase; - osg::ref_ptr mUpdater; - - void setPhase(const MoonState::Phase& phase) - { - if(mPhase == phase) - return; - - mPhase = phase; - - std::string textureName = "textures/tx_"; - - if (mType == Moon::Type_Secunda) - textureName += "secunda_"; - else - textureName += "masser_"; - - if (phase == MoonState::Phase::New) textureName += "new"; - else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; - else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; - else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; - else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; - else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; - else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; - else if(phase == MoonState::Phase::Full) textureName += "full"; - - textureName += ".dds"; - - if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); - else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); - } -}; - -SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) - : mSceneManager(sceneManager) - , mCamera(nullptr) - , mAtmosphereNightRoll(0.f) - , mCreated(false) - , mIsStorm(false) - , mDay(0) - , mMonth(0) - , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) - , mStormDirection(0,1,0) - , mClouds() - , mNextClouds() - , mCloudBlendFactor(0.0f) - , mCloudSpeed(0.0f) - , mStarsOpacity(0.0f) - , mRemainingTransitionTime(0.0f) - , mRainEnabled(false) - , mRainSpeed(0) - , mRainDiameter(0) - , mRainMinHeight(0) - , mRainMaxHeight(0) - , mRainEntranceSpeed(1) - , mRainMaxRaindrops(0) - , mWindSpeed(0.f) - , mBaseWindSpeed(0.f) - , mEnabled(true) - , mSunEnabled(true) - , mPrecipitationAlpha(0.f) -{ - osg::ref_ptr skyroot (new CameraRelativeTransform); - skyroot->setName("Sky Root"); - // Assign empty program to specify we don't want shaders - // The shaders generated by the SceneManager can't handle everything we need - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - - skyroot->setNodeMask(Mask_Sky); - parentNode->addChild(skyroot); - - mRootNode = skyroot; - - mEarlyRenderBinRoot = new osg::Group; - // render before the world is rendered - mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); - // Prevent unwanted clipping by water reflection camera's clipping plane - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); - mRootNode->addChild(mEarlyRenderBinRoot); - - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); -} - -void SkyManager::create() -{ - assert(!mCreated); - - mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); - ModVertexAlphaVisitor modAtmosphere(0); - mAtmosphereDay->accept(modAtmosphere); - - mAtmosphereUpdater = new AtmosphereUpdater; - mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - - mAtmosphereNightNode = new osg::PositionAttitudeTransform; - mAtmosphereNightNode->setNodeMask(0); - mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - - osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); - else - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); - atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - ModVertexAlphaVisitor modStars(2); - atmosphereNight->accept(modStars); - mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); - atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - - mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); - - mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); - mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); - - mCloudNode = new osg::PositionAttitudeTransform; - mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - ModVertexAlphaVisitor modClouds(1); - mCloudMesh->accept(modClouds); - mCloudUpdater = new CloudUpdater; - mCloudUpdater->setOpacity(1.f); - mCloudMesh->addUpdateCallback(mCloudUpdater); - - mCloudMesh2 = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - mCloudMesh2->accept(modClouds); - mCloudUpdater2 = new CloudUpdater; - mCloudUpdater2->setOpacity(0.f); - mCloudMesh2->addUpdateCallback(mCloudUpdater2); - mCloudMesh2->setNodeMask(0); - - auto depth = SceneUtil::createDepth(); - depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - - mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - - mCreated = true; -} - -class RainCounter : public osgParticle::ConstantRateCounter -{ -public: - int numParticlesToCreate(double dt) const override - { - // limit dt to avoid large particle emissions if there are jumps in the simulation time - // 0.2 seconds is the same cap as used in Engine's frame loop - dt = std::min(dt, 0.2); - return ConstantRateCounter::numParticlesToCreate(dt); - } -}; - -class RainShooter : public osgParticle::Shooter -{ -public: - RainShooter() - : mAngle(0.f) - { - } - - void shoot(osgParticle::Particle* particle) const override - { - particle->setVelocity(mVelocity); - particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); - } - - void setVelocity(const osg::Vec3f& velocity) - { - mVelocity = velocity; - } - - void setAngle(float angle) - { - mAngle = angle; - } - - osg::Object* cloneType() const override - { - return new RainShooter; - } - osg::Object* clone(const osg::CopyOp &) const override - { - return new RainShooter(*this); - } - -private: - osg::Vec3f mVelocity; - float mAngle; -}; - -// Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. -class AlphaFader : public SceneUtil::StateSetUpdater -{ -public: - /// @param alpha the variable alpha value is recovered from - AlphaFader(float& alpha) - : mAlpha(alpha) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - // need to create a deep copy of StateAttributes we will modify - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); - } - // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: - SetupVisitor(float &alpha) + SetupVisitor(const float &alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) - { - } + { } void apply(osg::Node &node) override { @@ -1320,7 +199,7 @@ public: callback = callback->getNestedCallback(); } - osg::ref_ptr alphaFader (new AlphaFader(mAlpha)); + osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); @@ -1333,608 +212,659 @@ public: } private: - float &mAlpha; + const float &mAlpha; }; - -protected: - float &mAlpha; -}; - -void SkyManager::setCamera(osg::Camera *camera) -{ - mCamera = camera; } -class WrapAroundOperator : public osgParticle::Operator +namespace MWRender { -public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator(), - mCamera(camera), mWrapRange(wrapRange), mHalfWrapRange(mWrapRange / 2.0) + SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) + : mSceneManager(sceneManager) + , mCamera(nullptr) + , mAtmosphereNightRoll(0.f) + , mCreated(false) + , mIsStorm(false) + , mDay(0) + , mMonth(0) + , mCloudAnimationTimer(0.f) + , mRainTimer(0.f) + , mStormParticleDirection(MWWorld::Weather::defaultDirection()) + , mStormDirection(MWWorld::Weather::defaultDirection()) + , mClouds() + , mNextClouds() + , mCloudBlendFactor(0.f) + , mCloudSpeed(0.f) + , mStarsOpacity(0.f) + , mRemainingTransitionTime(0.f) + , mRainEnabled(false) + , mRainSpeed(0.f) + , mRainDiameter(0.f) + , mRainMinHeight(0.f) + , mRainMaxHeight(0.f) + , mRainEntranceSpeed(1.f) + , mRainMaxRaindrops(0) + , mWindSpeed(0.f) + , mBaseWindSpeed(0.f) + , mEnabled(true) + , mSunEnabled(true) + , mPrecipitationAlpha(0.f) { - mPreviousCameraPosition = getCameraPosition(); + osg::ref_ptr skyroot = new CameraRelativeTransform; + skyroot->setName("Sky Root"); + // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline + if (!mSceneManager->getForceShaders()) + skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); + SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); + + skyroot->setNodeMask(Mask_Sky); + parentNode->addChild(skyroot); + + mRootNode = skyroot; + + mEarlyRenderBinRoot = new osg::Group; + // render before the world is rendered + mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + // Prevent unwanted clipping by water reflection camera's clipping plane + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); + mRootNode->addChild(mEarlyRenderBinRoot); + + mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); } - osg::Object *cloneType() const override + void SkyManager::create() { - return nullptr; - } + assert(!mCreated); - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; - } + bool forceShaders = mSceneManager->getForceShaders(); - void operate(osgParticle::Particle *P, double dt) override - { - } + mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); + ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); + mAtmosphereDay->accept(modAtmosphere); - void operateParticles(osgParticle::ParticleSystem *ps, double dt) override - { - osg::Vec3 position = getCameraPosition(); - osg::Vec3 positionDifference = position - mPreviousCameraPosition; + mAtmosphereUpdater = new AtmosphereUpdater; + mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - osg::Matrix toWorld, toLocal; + mAtmosphereNightNode = new osg::PositionAttitudeTransform; + mAtmosphereNightNode->setNodeMask(0); + mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - std::vector worldMatrices = ps->getWorldMatrices(); - - if (!worldMatrices.empty()) + osg::ref_ptr atmosphereNight; + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); + else + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); + atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); + atmosphereNight->accept(modStars); + mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); + atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); + + mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); + mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); + mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); + + mCloudNode = new osg::Group; + mEarlyRenderBinRoot->addChild(mCloudNode); + + mCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudMesh); + mCloudUpdater = new CloudUpdater(forceShaders); + mCloudUpdater->setOpacity(1.f); + cloudMeshChild->addUpdateCallback(mCloudUpdater); + mCloudMesh->addChild(cloudMeshChild); + + mNextCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr nextCloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mNextCloudMesh); + mNextCloudUpdater = new CloudUpdater(forceShaders); + mNextCloudUpdater->setOpacity(0.f); + nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); + mNextCloudMesh->setNodeMask(0); + mNextCloudMesh->addChild(nextCloudMeshChild); + + mCloudNode->addChild(mCloudMesh); + mCloudNode->addChild(mNextCloudMesh); + + ModVertexAlphaVisitor modClouds(ModVertexAlphaVisitor::Clouds); + mCloudMesh->accept(modClouds); + mNextCloudMesh->accept(modClouds); + + if (mSceneManager->getForceShaders()) { - toWorld = worldMatrices[0]; - toLocal.invert(toWorld); + auto vertex = mSceneManager->getShaderManager().getShader("sky_vertex.glsl", {}, osg::Shader::VERTEX); + auto fragment = mSceneManager->getShaderManager().getShader("sky_fragment.glsl", {}, osg::Shader::FRAGMENT); + auto program = mSceneManager->getShaderManager().getProgram(vertex, fragment); + mEarlyRenderBinRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", -1)); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } - for (int i = 0; i < ps->numParticles(); ++i) - { - osgParticle::Particle *p = ps->getParticle(i); - p->setPosition(toWorld.preMult(p->getPosition())); - p->setPosition(p->getPosition() - positionDifference); + auto depth = SceneUtil::createDepth(); + depth->setWriteMask(false); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions - { - osg::Vec3 pos = p->getPosition(); + mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - if (pos[j] < -mHalfWrapRange[j]) - pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); - else if (pos[j] > mHalfWrapRange[j]) - pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; - - p->setPosition(pos); - } - - p->setPosition(toLocal.preMult(p->getPosition())); - } - - mPreviousCameraPosition = position; + mCreated = true; } -protected: - osg::Camera *mCamera; - osg::Vec3 mPreviousCameraPosition; - osg::Vec3 mWrapRange; - osg::Vec3 mHalfWrapRange; - - osg::Vec3 getCameraPosition() - { - return mCamera->getInverseViewMatrix().getTrans(); - } -}; - -class WeatherAlphaOperator : public osgParticle::Operator -{ -public: - WeatherAlphaOperator(float& alpha, bool rain) - : mAlpha(alpha) - , mIsRain(rain) + void SkyManager::setCamera(osg::Camera *camera) { + mCamera = camera; } - osg::Object *cloneType() const override + void SkyManager::createRain() { - return nullptr; - } + if (mRainNode) + return; - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; - } - - void operate(osgParticle::Particle *particle, double dt) override - { - constexpr float rainThreshold = 0.6f; // Rain_Threshold? - const float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; - particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); - } - -private: - float &mAlpha; - bool mIsRain; -}; - -void SkyManager::createRain() -{ - if (mRainNode) - return; - - mRainNode = new osg::Group; - - mRainParticleSystem = new NifOsg::ParticleSystem; - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - - mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); - mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - - osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); - - osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); - raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); - stateset->setNestRenderBins(false); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - osg::ref_ptr mat (new osg::Material); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); - particleTemplate.setLifeTime(1); - - osg::ref_ptr emitter (new osgParticle::ModularEmitter); - emitter->setParticleSystem(mRainParticleSystem); - - osg::ref_ptr placer (new osgParticle::BoxPlacer); - placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - emitter->setPlacer(placer); - mPlacer = placer; - - // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. - // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). - // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. - osg::ref_ptr counter (new RainCounter); - counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); - emitter->setCounter(counter); - mCounter = counter; - - osg::ref_ptr shooter (new RainShooter); - mRainShooter = shooter; - emitter->setShooter(shooter); - - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mRainParticleSystem); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,rainRange)); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); - program->setParticleSystem(mRainParticleSystem); - mRainNode->addChild(program); - - mRainNode->addChild(emitter); - mRainNode->addChild(mRainParticleSystem); - mRainNode->addChild(updater); - - // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. - mRainNode->addCullCallback(mUnderwaterSwitch); - mRainNode->setNodeMask(Mask_WeatherParticles); - - mRootNode->addChild(mRainNode); -} - -void SkyManager::destroyRain() -{ - if (!mRainNode) - return; - - mRootNode->removeChild(mRainNode); - mRainNode = nullptr; - mPlacer = nullptr; - mCounter = nullptr; - mRainParticleSystem = nullptr; - mRainShooter = nullptr; -} - -SkyManager::~SkyManager() -{ - if (mRootNode) - { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; - } -} - -int SkyManager::getMasserPhase() const -{ - if (!mCreated) return 0; - return mMasser->getPhaseInt(); -} - -int SkyManager::getSecundaPhase() const -{ - if (!mCreated) return 0; - return mSecunda->getPhaseInt(); -} - -bool SkyManager::isEnabled() -{ - return mEnabled; -} - -bool SkyManager::hasRain() const -{ - return mRainNode != nullptr; -} - -float SkyManager::getPrecipitationAlpha() const -{ - if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) - return mPrecipitationAlpha; - - return 0.f; -} - -void SkyManager::update(float duration) -{ - if (!mEnabled) - return; - - switchUnderwaterRain(); - - if (mIsStorm) - { - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); - - mCloudNode->setAttitude(quat); - if (mParticleNode) - { - // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) - quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); - mParticleNode->setAttitude(quat); - } - } - else - mCloudNode->setAttitude(osg::Quat()); - - // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; - mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); - mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); - - // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); - if (mAtmosphereNightNode->getNodeMask() != 0) - mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); -} - -void SkyManager::setEnabled(bool enabled) -{ - if (enabled && !mCreated) - create(); - - mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); - - mEnabled = enabled; -} - -void SkyManager::setMoonColour (bool red) -{ - if (!mCreated) return; - mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); -} - -void SkyManager::updateRainParameters() -{ - if (mRainShooter) - { - float angle = -std::atan(mWindSpeed/50.f); - mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); - mRainShooter->setAngle(angle); + mRainNode = new osg::Group; + mRainParticleSystem = new NifOsg::ParticleSystem; osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); + mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); + + osg::ref_ptr raindropTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds")); + raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); + stateset->setNestRenderBins(false); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + + osg::ref_ptr mat = new osg::Material; + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + + osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); + particleTemplate.setLifeTime(1); + + osg::ref_ptr emitter = new osgParticle::ModularEmitter; + emitter->setParticleSystem(mRainParticleSystem); + + osg::ref_ptr placer = new osgParticle::BoxPlacer; + placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + emitter->setPlacer(placer); + mPlacer = placer; + + // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. + // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). + // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. + osg::ref_ptr counter = new RainCounter; + counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + emitter->setCounter(counter); + mCounter = counter; + + osg::ref_ptr shooter = new RainShooter; + mRainShooter = shooter; + emitter->setShooter(shooter); + + osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; + updater->addParticleSystem(mRainParticleSystem); + + osg::ref_ptr program = new osgParticle::ModularProgram; + program->addOperator(new WrapAroundOperator(mCamera,rainRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); + program->setParticleSystem(mRainParticleSystem); + mRainNode->addChild(program); + + mRainNode->addChild(emitter); + mRainNode->addChild(mRainParticleSystem); + mRainNode->addChild(updater); + + // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. + mRainNode->addCullCallback(mUnderwaterSwitch); + mRainNode->setNodeMask(Mask_WeatherParticles); + + mRainParticleSystem->setUserValue("simpleLighting", true); + mSceneManager->recreateShaders(mRainNode); + + mRootNode->addChild(mRainNode); } -} -void SkyManager::switchUnderwaterRain() -{ - if (!mRainParticleSystem) - return; - - bool freeze = mUnderwaterSwitch->isUnderwater(); - mRainParticleSystem->setFrozen(freeze); -} - -void SkyManager::setWeather(const WeatherResult& weather) -{ - if (!mCreated) return; - - mRainEntranceSpeed = weather.mRainEntranceSpeed; - mRainMaxRaindrops = weather.mRainMaxRaindrops; - mRainDiameter = weather.mRainDiameter; - mRainMinHeight = weather.mRainMinHeight; - mRainMaxHeight = weather.mRainMaxHeight; - mRainSpeed = weather.mRainSpeed; - mWindSpeed = weather.mWindSpeed; - mBaseWindSpeed = weather.mBaseWindSpeed; - - if (mRainEffect != weather.mRainEffect) + void SkyManager::destroyRain() { - mRainEffect = weather.mRainEffect; - if (!mRainEffect.empty()) + if (!mRainNode) + return; + + mRootNode->removeChild(mRainNode); + mRainNode = nullptr; + mPlacer = nullptr; + mCounter = nullptr; + mRainParticleSystem = nullptr; + mRainShooter = nullptr; + } + + SkyManager::~SkyManager() + { + if (mRootNode) { - createRain(); - } - else - { - destroyRain(); + mRootNode->getParent(0)->removeChild(mRootNode); + mRootNode = nullptr; } } - updateRainParameters(); - - mIsStorm = weather.mIsStorm; - - if (mCurrentParticleEffect != weather.mParticleEffect) + int SkyManager::getMasserPhase() const { - mCurrentParticleEffect = weather.mParticleEffect; + if (!mCreated) return 0; + return mMasser->getPhaseInt(); + } - // cleanup old particles - if (mParticleEffect) + int SkyManager::getSecundaPhase() const + { + if (!mCreated) return 0; + return mSecunda->getPhaseInt(); + } + + bool SkyManager::isEnabled() + { + return mEnabled; + } + + bool SkyManager::hasRain() const + { + return mRainNode != nullptr; + } + + float SkyManager::getPrecipitationAlpha() const + { + if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) + return mPrecipitationAlpha; + + return 0.f; + } + + void SkyManager::update(float duration) + { + if (!mEnabled) + return; + + switchUnderwaterRain(); + + if (mIsStorm && mParticleNode) { - mParticleNode->removeChild(mParticleEffect); - mParticleEffect = nullptr; + osg::Quat quat; + quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); + // Morrowind deliberately rotates the blizzard mesh, so so should we. + if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) + quat.makeRotate(osg::Vec3f(-1,0,0), mStormParticleDirection); + mParticleNode->setAttitude(quat); } - if (mCurrentParticleEffect.empty()) + // UV Scroll the clouds + mCloudAnimationTimer += duration * mCloudSpeed * 0.003; + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); + mCloudUpdater->setTextureCoord(mCloudAnimationTimer); + + // morrowind rotates each cloud mesh independently + osg::Quat rotation; + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); + mCloudMesh->setAttitude(rotation); + + if (mNextCloudMesh->getNodeMask()) { - if (mParticleNode) + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); + mNextCloudMesh->setAttitude(rotation); + } + + // rotate the stars by 360 degrees every 4 days + mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); + if (mAtmosphereNightNode->getNodeMask() != 0) + mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); + } + + void SkyManager::setEnabled(bool enabled) + { + if (enabled && !mCreated) + create(); + + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); + + if (!enabled && mParticleNode && mParticleEffect) + mCurrentParticleEffect = {}; + + mEnabled = enabled; + } + + void SkyManager::setMoonColour (bool red) + { + if (!mCreated) return; + mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); + } + + void SkyManager::updateRainParameters() + { + if (mRainShooter) + { + float angle = -std::atan(mWindSpeed/50.f); + mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); + mRainShooter->setAngle(angle); + + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); + + mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + + mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + } + } + + void SkyManager::switchUnderwaterRain() + { + if (!mRainParticleSystem) + return; + + bool freeze = mUnderwaterSwitch->isUnderwater(); + mRainParticleSystem->setFrozen(freeze); + } + + void SkyManager::setWeather(const WeatherResult& weather) + { + if (!mCreated) return; + + mRainEntranceSpeed = weather.mRainEntranceSpeed; + mRainMaxRaindrops = weather.mRainMaxRaindrops; + mRainDiameter = weather.mRainDiameter; + mRainMinHeight = weather.mRainMinHeight; + mRainMaxHeight = weather.mRainMaxHeight; + mRainSpeed = weather.mRainSpeed; + mWindSpeed = weather.mWindSpeed; + mBaseWindSpeed = weather.mBaseWindSpeed; + + if (mRainEffect != weather.mRainEffect) + { + mRainEffect = weather.mRainEffect; + if (!mRainEffect.empty()) { - mRootNode->removeChild(mParticleNode); - mParticleNode = nullptr; + createRain(); + } + else + { + destroyRain(); } } - else + + updateRainParameters(); + + mIsStorm = weather.mIsStorm; + + if (mIsStorm) + mStormDirection = weather.mStormDirection; + + if (mCurrentParticleEffect != weather.mParticleEffect) { - if (!mParticleNode) + mCurrentParticleEffect = weather.mParticleEffect; + + // cleanup old particles + if (mParticleEffect) { - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->addCullCallback(mUnderwaterSwitch); - mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; } - mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); - - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); - mParticleEffect->accept(assignVisitor); - - AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); - - mParticleEffect->accept(alphaFaderSetupVisitor); - - SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); - mParticleEffect->accept(findPSVisitor); - - for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + if (mCurrentParticleEffect.empty()) { - osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); - - osg::ref_ptr program (new osgParticle::ModularProgram); - if (!mIsStorm) - program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); - program->setParticleSystem(ps); - mParticleNode->addChild(program); + if (mParticleNode) + { + mRootNode->removeChild(mParticleNode); + mParticleNode = nullptr; + } + } + else + { + if (!mParticleNode) + { + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->addCullCallback(mUnderwaterSwitch); + mParticleNode->setNodeMask(Mask_WeatherParticles); + mParticleNode->getOrCreateStateSet(); + mRootNode->addChild(mParticleNode); + } + + mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + + SceneUtil::AssignControllerSourcesVisitor assignVisitor = std::shared_ptr(new SceneUtil::FrameTimeSource); + mParticleEffect->accept(assignVisitor); + + SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mParticleEffect->accept(alphaFaderSetupVisitor); + + SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); + mParticleEffect->accept(findPSVisitor); + + for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + { + osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); + + osg::ref_ptr program = new osgParticle::ModularProgram; + if (!mIsStorm) + program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); + program->setParticleSystem(ps); + mParticleNode->addChild(program); + + for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + ps->getParticle(particleIndex)->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + + ps->getOrCreateStateSet(); + ps->setUserValue("simpleLighting", true); + } + + mSceneManager->recreateShaders(mParticleNode); } } - } - if (mClouds != weather.mCloudTexture) - { - mClouds = weather.mCloudTexture; - - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); - - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); - cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - - mCloudUpdater->setTexture(cloudTex); - } - - if (mNextClouds != weather.mNextCloudTexture) - { - mNextClouds = weather.mNextCloudTexture; - - if (!mNextClouds.empty()) + if (mClouds != weather.mCloudTexture) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + mClouds = weather.mCloudTexture; - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); + std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mCloudUpdater2->setTexture(cloudTex); + mCloudUpdater->setTexture(cloudTex); } + + if (mStormDirection != weather.mStormDirection) + mStormDirection = weather.mStormDirection; + + if (mNextStormDirection != weather.mNextStormDirection) + mNextStormDirection = weather.mNextStormDirection; + + if (mNextClouds != weather.mNextCloudTexture) + { + mNextClouds = weather.mNextCloudTexture; + + if (!mNextClouds.empty()) + { + std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); + cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + + mNextCloudUpdater->setTexture(cloudTex); + mNextStormDirection = weather.mStormDirection; + } + } + + if (mCloudBlendFactor != weather.mCloudBlendFactor) + { + mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); + + mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); + mNextCloudUpdater->setOpacity(mCloudBlendFactor); + mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + } + + if (mCloudColour != weather.mFogColor) + { + osg::Vec4f clr (weather.mFogColor); + clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + + mCloudUpdater->setEmissionColor(clr); + mNextCloudUpdater->setEmissionColor(clr); + + mCloudColour = weather.mFogColor; + } + + if (mSkyColour != weather.mSkyColor) + { + mSkyColour = weather.mSkyColor; + + mAtmosphereUpdater->setEmissionColor(mSkyColour); + mMasser->setAtmosphereColor(mSkyColour); + mSecunda->setAtmosphereColor(mSkyColour); + } + + if (mFogColour != weather.mFogColor) + { + mFogColour = weather.mFogColor; + } + + mCloudSpeed = weather.mCloudSpeed; + + mMasser->adjustTransparency(weather.mGlareView); + mSecunda->adjustTransparency(weather.mGlareView); + + mSun->setColor(weather.mSunDiscColor); + mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + + float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + + if (weather.mNight && mStarsOpacity != nextStarsOpacity) + { + mStarsOpacity = nextStarsOpacity; + + mAtmosphereNightUpdater->setFade(mStarsOpacity); + } + + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + mPrecipitationAlpha = weather.mPrecipitationAlpha; } - if (mCloudBlendFactor != weather.mCloudBlendFactor) + float SkyManager::getBaseWindSpeed() const { - mCloudBlendFactor = weather.mCloudBlendFactor; + if (!mCreated) return 0.f; - mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); - mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + return mBaseWindSpeed; } - if (mCloudColour != weather.mFogColor) + void SkyManager::sunEnable() { - osg::Vec4f clr (weather.mFogColor); - clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + if (!mCreated) return; - mCloudUpdater->setEmissionColor(clr); - mCloudUpdater2->setEmissionColor(clr); - - mCloudColour = weather.mFogColor; + mSun->setVisible(true); } - if (mSkyColour != weather.mSkyColor) + void SkyManager::sunDisable() { - mSkyColour = weather.mSkyColor; + if (!mCreated) return; - mAtmosphereUpdater->setEmissionColor(mSkyColour); - mMasser->setAtmosphereColor(mSkyColour); - mSecunda->setAtmosphereColor(mSkyColour); + mSun->setVisible(false); } - if (mFogColour != weather.mFogColor) + void SkyManager::setStormParticleDirection(const osg::Vec3f &direction) { - mFogColour = weather.mFogColor; + mStormParticleDirection = direction; } - mCloudSpeed = weather.mCloudSpeed; - - mMasser->adjustTransparency(weather.mGlareView); - mSecunda->adjustTransparency(weather.mGlareView); - - mSun->setColor(weather.mSunDiscColor); - mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); - - float nextStarsOpacity = weather.mNightFade * weather.mGlareView; - - if (weather.mNight && mStarsOpacity != nextStarsOpacity) + void SkyManager::setSunDirection(const osg::Vec3f& direction) { - mStarsOpacity = nextStarsOpacity; + if (!mCreated) return; - mAtmosphereNightUpdater->setFade(mStarsOpacity); + mSun->setDirection(direction); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); - - mPrecipitationAlpha = weather.mPrecipitationAlpha; -} - -float SkyManager::getBaseWindSpeed() const -{ - if (!mCreated) return 0.f; - - return mBaseWindSpeed; -} - -void SkyManager::sunEnable() -{ - if (!mCreated) return; - - mSun->setVisible(true); -} - -void SkyManager::sunDisable() -{ - if (!mCreated) return; - - mSun->setVisible(false); -} - -void SkyManager::setStormDirection(const osg::Vec3f &direction) -{ - mStormDirection = direction; -} - -void SkyManager::setSunDirection(const osg::Vec3f& direction) -{ - if (!mCreated) return; - - mSun->setDirection(direction); -} - -void SkyManager::setMasserState(const MoonState& state) -{ - if(!mCreated) return; - - mMasser->setState(state); -} - -void SkyManager::setSecundaState(const MoonState& state) -{ - if(!mCreated) return; - - mSecunda->setState(state); -} - -void SkyManager::setDate(int day, int month) -{ - mDay = day; - mMonth = month; -} - -void SkyManager::setGlareTimeOfDayFade(float val) -{ - mSun->setGlareTimeOfDayFade(val); -} - -void SkyManager::setWaterHeight(float height) -{ - mUnderwaterSwitch->setWaterLevel(height); -} - -void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) -{ - models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - models.emplace_back(Settings::Manager::getString("skynight02", "Models")); - models.emplace_back(Settings::Manager::getString("skynight01", "Models")); - models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); - - models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); - - textures.emplace_back("textures/tx_mooncircle_full_s.dds"); - textures.emplace_back("textures/tx_mooncircle_full_m.dds"); - - textures.emplace_back("textures/tx_masser_new.dds"); - textures.emplace_back("textures/tx_masser_one_wax.dds"); - textures.emplace_back("textures/tx_masser_half_wax.dds"); - textures.emplace_back("textures/tx_masser_three_wax.dds"); - textures.emplace_back("textures/tx_masser_one_wan.dds"); - textures.emplace_back("textures/tx_masser_half_wan.dds"); - textures.emplace_back("textures/tx_masser_three_wan.dds"); - textures.emplace_back("textures/tx_masser_full.dds"); - - textures.emplace_back("textures/tx_secunda_new.dds"); - textures.emplace_back("textures/tx_secunda_one_wax.dds"); - textures.emplace_back("textures/tx_secunda_half_wax.dds"); - textures.emplace_back("textures/tx_secunda_three_wax.dds"); - textures.emplace_back("textures/tx_secunda_one_wan.dds"); - textures.emplace_back("textures/tx_secunda_half_wan.dds"); - textures.emplace_back("textures/tx_secunda_three_wan.dds"); - textures.emplace_back("textures/tx_secunda_full.dds"); - - textures.emplace_back("textures/tx_sun_05.dds"); - textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); - - textures.emplace_back("textures/tx_raindrop_01.dds"); -} - -void SkyManager::setWaterEnabled(bool enabled) -{ - mUnderwaterSwitch->setEnabled(enabled); -} + void SkyManager::setMasserState(const MoonState& state) + { + if(!mCreated) return; + mMasser->setState(state); + } + + void SkyManager::setSecundaState(const MoonState& state) + { + if(!mCreated) return; + + mSecunda->setState(state); + } + + void SkyManager::setDate(int day, int month) + { + mDay = day; + mMonth = month; + } + + void SkyManager::setGlareTimeOfDayFade(float val) + { + mSun->setGlareTimeOfDayFade(val); + } + + void SkyManager::setWaterHeight(float height) + { + mUnderwaterSwitch->setWaterLevel(height); + } + + void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) + { + models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + models.emplace_back(Settings::Manager::getString("skynight02", "Models")); + models.emplace_back(Settings::Manager::getString("skynight01", "Models")); + models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); + + models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); + + textures.emplace_back("textures/tx_mooncircle_full_s.dds"); + textures.emplace_back("textures/tx_mooncircle_full_m.dds"); + + textures.emplace_back("textures/tx_masser_new.dds"); + textures.emplace_back("textures/tx_masser_one_wax.dds"); + textures.emplace_back("textures/tx_masser_half_wax.dds"); + textures.emplace_back("textures/tx_masser_three_wax.dds"); + textures.emplace_back("textures/tx_masser_one_wan.dds"); + textures.emplace_back("textures/tx_masser_half_wan.dds"); + textures.emplace_back("textures/tx_masser_three_wan.dds"); + textures.emplace_back("textures/tx_masser_full.dds"); + + textures.emplace_back("textures/tx_secunda_new.dds"); + textures.emplace_back("textures/tx_secunda_one_wax.dds"); + textures.emplace_back("textures/tx_secunda_half_wax.dds"); + textures.emplace_back("textures/tx_secunda_three_wax.dds"); + textures.emplace_back("textures/tx_secunda_one_wan.dds"); + textures.emplace_back("textures/tx_secunda_half_wan.dds"); + textures.emplace_back("textures/tx_secunda_three_wan.dds"); + textures.emplace_back("textures/tx_secunda_full.dds"); + + textures.emplace_back("textures/tx_sun_05.dds"); + textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); + + textures.emplace_back("textures/tx_raindrop_01.dds"); + } + + void SkyManager::setWaterEnabled(bool enabled) + { + mUnderwaterSwitch->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index f8c501dda6..227dee213b 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -8,10 +8,7 @@ #include #include -namespace osg -{ - class Camera; -} +#include "skyutil.hpp" namespace osg { @@ -19,6 +16,7 @@ namespace osg class Node; class Material; class PositionAttitudeTransform; + class Camera; } namespace osgParticle @@ -45,80 +43,6 @@ namespace MWRender class AlphaFader; class UnderwaterSwitchCallback; - struct WeatherResult - { - std::string mCloudTexture; - std::string mNextCloudTexture; - float mCloudBlendFactor; - - osg::Vec4f mFogColor; - - osg::Vec4f mAmbientColor; - - osg::Vec4f mSkyColor; - - // sun light color - osg::Vec4f mSunColor; - - // alpha is the sun transparency - osg::Vec4f mSunDiscColor; - - float mFogDepth; - - float mDLFogFactor; - float mDLFogOffset; - - float mWindSpeed; - float mBaseWindSpeed; - float mCurrentWindSpeed; - float mNextWindSpeed; - - float mCloudSpeed; - - float mGlareView; - - bool mNight; // use night skybox - float mNightFade; // fading factor for night skybox - - bool mIsStorm; - - std::string mAmbientLoopSoundID; - float mAmbientSoundVolume; - - std::string mParticleEffect; - std::string mRainEffect; - float mPrecipitationAlpha; - - float mRainDiameter; - float mRainMinHeight; - float mRainMaxHeight; - float mRainSpeed; - float mRainEntranceSpeed; - int mRainMaxRaindrops; - }; - - struct MoonState - { - enum class Phase - { - Full = 0, - WaningGibbous, - ThirdQuarter, - WaningCrescent, - New, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - Unspecified - }; - - float mRotationFromHorizon; - float mRotationFromNorth; - Phase mPhase; - float mShadowBlend; - float mMoonAlpha; - }; - ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager @@ -162,7 +86,7 @@ namespace MWRender void setRainSpeed(float speed); - void setStormDirection(const osg::Vec3f& direction); + void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); @@ -203,12 +127,12 @@ namespace MWRender osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; - osg::ref_ptr mCloudNode; + osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; - osg::ref_ptr mCloudUpdater2; - osg::ref_ptr mCloudMesh; - osg::ref_ptr mCloudMesh2; + osg::ref_ptr mNextCloudUpdater; + osg::ref_ptr mCloudMesh; + osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; @@ -239,7 +163,10 @@ namespace MWRender float mRainTimer; + // particle system rotation is independent of cloud rotation internally + osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; @@ -275,4 +202,4 @@ namespace MWRender }; } -#endif // GAME_RENDER_SKY_H +#endif diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp new file mode 100644 index 0000000000..ae689547fd --- /dev/null +++ b/apps/openmw/mwrender/skyutil.cpp @@ -0,0 +1,1142 @@ +#include "skyutil.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include + +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/weather.hpp" + +#include "vismask.hpp" +#include "renderbin.hpp" + +namespace +{ + enum class Pass + { + Atmosphere, + Atmosphere_Night, + Clouds, + Moon, + Sun, + Sunflash_Query, + Sunglare, + }; + + osg::ref_ptr createTexturedQuad(int numUvSets = 1, float scale = 1.f) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-0.5 * scale, -0.5 * scale, 0)); + verts->push_back(osg::Vec3f(-0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, -0.5 * scale, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(0, 1)); + texcoords->push_back(osg::Vec2f(0, 0)); + texcoords->push_back(osg::Vec2f(1, 0)); + texcoords->push_back(osg::Vec2f(1, 1)); + + osg::ref_ptr colors = new osg::Vec4Array; + colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); + geom->setColorArray(colors, osg::Array::BIND_OVERALL); + + for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + + return geom; + } + + struct DummyComputeBoundCallback : osg::Node::ComputeBoundingSphereCallback + { + osg::BoundingSphere computeBound(const osg::Node& node) const override + { + return osg::BoundingSphere(); + } + }; +} + +namespace MWRender +{ + osg::ref_ptr createAlphaTrackingUnlitMaterial() + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(osg::Material::DIFFUSE); + return mat; + } + + osg::ref_ptr createUnlitMaterial() + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(osg::Material::OFF); + return mat; + } + + class SunUpdater : public SceneUtil::StateSetUpdater + { + public: + osg::Vec4f mColor; + + SunUpdater() + : mColor(1.f, 1.f, 1.f, 1.f) + { } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + } + }; + + OcclusionCallback::OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : mOcclusionQueryVisiblePixels(oqnVisible) + , mOcclusionQueryTotalPixels(oqnTotal) + { } + + float OcclusionCallback::getVisibleRatio (osg::Camera* camera) + { + int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); + int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); + + float visibleRatio = 0.f; + if (total > 0) + visibleRatio = static_cast(visible) / static_cast(total); + + float dt = MWBase::Environment::get().getFrameDuration(); + + float lastRatio = mLastRatio[osg::observer_ptr(camera)]; + + float change = dt*10; + + if (visibleRatio > lastRatio) + visibleRatio = std::min(visibleRatio, lastRatio + change); + else + visibleRatio = std::max(visibleRatio, lastRatio - change); + + mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + + return visibleRatio; + } + + /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. + class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : OcclusionCallback(oqnVisible, oqnTotal) + , mGlareView(1.f) + { } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + osg::ref_ptr stateset; + + if (visibleRatio > 0.f) + { + const float fadeThreshold = 0.1; + if (visibleRatio < fadeThreshold) + { + float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; + osg::ref_ptr mat (MWRender::createUnlitMaterial()); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); + stateset = new osg::StateSet; + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } + } + + float scale = visibleRatio; + + if (scale == 0.f) + { + // no traverse + return; + } + else if (scale == 1.f) + traverse(node, cv); + else + { + osg::Matrix modelView = *cv->getModelViewMatrix(); + + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); + + if (stateset) + cv->pushStateSet(stateset); + + cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + + traverse(node, cv); + + cv->popModelViewMatrix(); + + if (stateset) + cv->popStateSet(); + } + } + + void setGlareView(float value) + { + mGlareView = value; + } + + private: + float mGlareView; + }; + + /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. + /// Must be attached as a cull callback to the node above the glare node. + class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, + osg::ref_ptr sunTransform) + : OcclusionCallback(oqnVisible, oqnTotal) + , mSunTransform(sunTransform) + , mTimeOfDayFade(1.f) + , mGlareView(1.f) + { + mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); + mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); + mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); + + // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, + // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, + // so the resulting color looks more orange than red. + mColor *= 2; + for (int i=0; i<3; ++i) + mColor[i] = std::min(1.f, mColor[i]); + } + + void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); + + float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); + float fade = value * mSunGlareFaderMax; + + fade *= mTimeOfDayFade * mGlareView * visibleRatio; + + if (fade == 0.f) + { + // no traverse + return; + + osg::Vec4 v4; + osg::Vec4 v3; + v4 = v3; + } + else + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr mat = MWRender::createUnlitMaterial(); + + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } + } + + void setTimeOfDayFade(float val) + { + mTimeOfDayFade = val; + } + + void setGlareView(float glareView) + { + mGlareView = glareView; + } + + private: + float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + { + osg::Vec3d eye, center, up; + viewMatrix.getLookAt(eye, center, up); + + osg::Vec3d forward = center - eye; + osg::Vec3d sun = mSunTransform->getPosition(); + + forward.normalize(); + sun.normalize(); + float angleRadians = std::acos(forward * sun); + return angleRadians; + } + + osg::ref_ptr mSunTransform; + float mTimeOfDayFade; + float mGlareView; + osg::Vec4f mColor; + float mSunGlareFaderMax; + float mSunGlareFaderAngleMax; + }; + + struct MoonUpdater : SceneUtil::StateSetUpdater + { + Resource::ImageManager& mImageManager; + osg::ref_ptr mPhaseTex; + osg::ref_ptr mCircleTex; + float mTransparency; + float mShadowBlend; + osg::Vec4f mAtmosphereColor; + osg::Vec4f mMoonColor; + bool mForceShaders; + + MoonUpdater(Resource::ImageManager& imageManager, bool forceShaders) + : mImageManager(imageManager) + , mPhaseTex() + , mCircleTex() + , mTransparency(1.0f) + , mShadowBlend(1.0f) + , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) + , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + , mForceShaders(forceShaders) + { } + + void setDefaults(osg::StateSet* stateset) override + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("diffuseMap", 0)); + stateset->addUniform(new osg::Uniform("maskMap", 1)); + stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor + stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); + + stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); + osg::ref_ptr texEnv2 = new osg::TexEnvCombine; + texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); + texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); + texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency + stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); + stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + if (mForceShaders) + { + stateset->setTextureAttribute(0, mPhaseTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(1, mCircleTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + if (auto* uMoonBlend = stateset->getUniform("moonBlend")) + uMoonBlend->set(mMoonColor * mShadowBlend); + if (auto* uAtmosphereFade = stateset->getUniform("atmosphereFade")) + uAtmosphereFade->set(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mMoonColor * mShadowBlend); + + osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + } + + void setTextures(const std::string& phaseTex, const std::string& circleTex) + { + mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); + mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); + mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reset(); + } + }; + + class CameraRelativeTransformCullCallback : public SceneUtil::NodeCallback + { + public: + void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + { + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, cv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; + + void AtmosphereUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + + void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + } + + AtmosphereNightUpdater::AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders) + : mColor(osg::Vec4f(0,0,0,0)) + , mTexture(new osg::Texture2D(imageManager->getWarningImage())) + , mForceShaders(forceShaders) + { } + + void AtmosphereNightUpdater::setFade(float fade) + { + mColor.a() = fade; + } + + void AtmosphereNightUpdater::setDefaults(osg::StateSet* stateset) + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("opacity", 0.f), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + + stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void AtmosphereNightUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mColor.a()); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mColor); + } + } + + CloudUpdater::CloudUpdater(bool forceShaders) + : mOpacity(0.f) + , mForceShaders(forceShaders) + { } + + void CloudUpdater::setTexture(osg::ref_ptr texture) + { + mTexture = texture; + } + + void CloudUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void CloudUpdater::setOpacity(float opacity) + { + mOpacity = opacity; + } + + void CloudUpdater::setTextureCoord(float timer) + { + mTexMat = osg::Matrixf::translate(osg::Vec3f(0.f, -timer, 0.f)); + } + + void CloudUpdater::setDefaults(osg::StateSet *stateset) + { + stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr texmat = new osg::TexMat; + stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); + + if (mForceShaders) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + stateset->addUniform(new osg::Uniform("opacity", 1.f), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); + // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already + osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; + texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); + texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); + + stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); + + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void CloudUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + + osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); + texMat->setMatrix(mTexMat); + + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mOpacity); + } + else + { + stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); + } + } + + CameraRelativeTransform::CameraRelativeTransform() + { + // Culling works in node-local space, not in camera space, so we can't cull this node correctly + // That's not a problem though, children of this node can be culled just fine + // Just make sure you do not place a CameraRelativeTransform deep in the scene graph + setCullingActive(false); + + addCullCallback(new CameraRelativeTransformCullCallback); + } + + CameraRelativeTransform::CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) + : osg::Transform(copy, copyop) + { } + + const osg::Vec3f& CameraRelativeTransform::getLastViewPoint() const + { + return mViewPoint; + } + + bool CameraRelativeTransform::computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const + { + if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + mViewPoint = static_cast(nv)->getViewPoint(); + } + + if (_referenceFrame==RELATIVE_RF) + { + matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); + return false; + } + else // absolute + { + matrix.makeIdentity(); + return true; + } + } + + osg::BoundingSphere CameraRelativeTransform::computeBound() const + { + return osg::BoundingSphere(); + } + + UnderwaterSwitchCallback::UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) + : mCameraRelativeTransform(cameraRelativeTransform) + , mEnabled(true) + , mWaterLevel(0.f) + { } + + bool UnderwaterSwitchCallback::isUnderwater() + { + osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); + return mEnabled && viewPoint.z() < mWaterLevel; + } + + void UnderwaterSwitchCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (isUnderwater()) + return; + + traverse(node, nv); + } + + void UnderwaterSwitchCallback::setEnabled(bool enabled) + { + mEnabled = enabled; + } + void UnderwaterSwitchCallback::setWaterLevel(float waterLevel) + { + mWaterLevel = waterLevel; + } + + const float CelestialBody::mDistance = 1000.0f; + + CelestialBody::CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask) + : mVisibleMask(visibleMask) + { + mGeom = createTexturedQuad(numUvSets); + mGeom->getOrCreateStateSet(); + mTransform = new osg::PositionAttitudeTransform; + mTransform->setNodeMask(mVisibleMask); + mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); + mTransform->addChild(mGeom); + + parentNode->addChild(mTransform); + } + + void CelestialBody::setVisible(bool visible) + { + mTransform->setNodeMask(visible ? mVisibleMask : 0); + } + + Sun::Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) + : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) + , mUpdater(new SunUpdater) + { + mTransform->addUpdateCallback(mUpdater); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds")); + sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + sunTex->setName("diffuseMap"); + + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); + mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr queryNode = new osg::Group; + // Need to render after the world geometry so we can correctly test for occlusions + osg::StateSet* stateset = queryNode->getOrCreateStateSet(); + stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); + stateset->setNestRenderBins(false); + // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun + osg::ref_ptr alphaFunc = new osg::AlphaFunc; + alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); + stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + // Disable writing to the color buffer. We are using this geometry for visibility tests only. + osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); + stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); + + mTransform->addChild(queryNode); + + mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); + mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); + + createSunFlash(imageManager); + createSunGlare(); + } + + Sun::~Sun() + { + mTransform->removeUpdateCallback(mUpdater); + destroySunFlash(); + destroySunGlare(); + } + + void Sun::setColor(const osg::Vec4f& color) + { + mUpdater->mColor.r() = color.r(); + mUpdater->mColor.g() = color.g(); + mUpdater->mColor.b() = color.b(); + } + + void Sun::adjustTransparency(const float ratio) + { + mUpdater->mColor.a() = ratio; + if (mSunGlareCallback) + mSunGlareCallback->setGlareView(ratio); + if (mSunFlashCallback) + mSunFlashCallback->setGlareView(ratio); + } + + void Sun::setDirection(const osg::Vec3f& direction) + { + osg::Vec3f normalizedDirection = direction / direction.length(); + mTransform->setPosition(normalizedDirection * mDistance); + + osg::Quat quat; + quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); + mTransform->setAttitude(quat); + } + + void Sun::setGlareTimeOfDayFade(float val) + { + if (mSunGlareCallback) + mSunGlareCallback->setTimeOfDayFade(val); + } + + osg::ref_ptr Sun::createOcclusionQueryNode(osg::Group* parent, bool queryVisible) + { + osg::ref_ptr oqn = new osg::OcclusionQueryNode; + oqn->setQueriesEnabled(true); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced + osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); +#else + osg::ref_ptr queryGeom = oqn->getQueryGeometry(); +#endif + + // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, + // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry + // is only called once. + // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. + queryGeom->setDataVariance(osg::Object::STATIC); + + // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, + // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to + // circumvent this. + queryGeom->setVertexArray(mGeom->getVertexArray()); + queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); + queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); + queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); + + // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. + oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); + // Still need a proper bounding sphere. + oqn->setInitialBound(queryGeom->getBound()); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + oqn->setQueryGeometry(queryGeom.release()); +#endif + + osg::StateSet* queryStateSet = new osg::StateSet; + if (queryVisible) + { + auto depth = SceneUtil::createDepth(); + // This is a trick to make fragments written by the query always use the maximum depth value, + // without having to retrieve the current far clipping distance. + // We want the sun glare to be "infinitely" far away. + double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; + depth->setZNear(far); + depth->setZFar(far); + depth->setWriteMask(false); + queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + else + { + queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + oqn->setQueryStateSet(queryStateSet); + + parent->addChild(oqn); + + return oqn; + } + + void Sun::createSunFlash(Resource::ImageManager& imageManager) + { + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds")); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + tex->setName("diffuseMap"); + + osg::ref_ptr group (new osg::Group); + + mTransform->addChild(group); + + const float scale = 2.6f; + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + mSunFlashNode = group; + + mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); + mSunFlashNode->addCullCallback(mSunFlashCallback); + } + + void Sun::destroySunFlash() + { + if (mSunFlashNode) + { + mSunFlashNode->removeCullCallback(mSunFlashCallback); + mSunFlashCallback = nullptr; + } + } + + void Sun::createSunGlare() + { + osg::ref_ptr camera = new osg::Camera; + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); + camera->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + // set up additive blending + osg::ref_ptr blendFunc = new osg::BlendFunc; + blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); + blendFunc->setDestination(osg::BlendFunc::ONE); + stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + + mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); + mSunGlareNode = camera; + + mSunGlareNode->addCullCallback(mSunGlareCallback); + + mTransform->addChild(camera); + } + + void Sun::destroySunGlare() + { + if (mSunGlareNode) + { + mSunGlareNode->removeCullCallback(mSunGlareCallback); + mSunGlareCallback = nullptr; + } + } + + Moon::Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type) + : CelestialBody(parentNode, scaleFactor, 2) + , mType(type) + , mPhase(MoonState::Phase::Unspecified) + , mUpdater(new MoonUpdater(*sceneManager.getImageManager(), sceneManager.getForceShaders())) + { + setPhase(MoonState::Phase::Full); + setVisible(true); + + mGeom->addUpdateCallback(mUpdater); + } + + Moon::~Moon() + { + mGeom->removeUpdateCallback(mUpdater); + } + + void Moon::adjustTransparency(const float ratio) + { + mUpdater->mTransparency *= ratio; + } + + void Moon::setState(const MoonState state) + { + float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; + float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; + + osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); + + osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); + mTransform->setPosition(direction * mDistance); + + // The moon quad is initially oriented facing down, so we need to offset its X-axis + // rotation to rotate it to face the camera when sitting at the horizon. + osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + mTransform->setAttitude(attX * rotZ); + + setPhase(state.mPhase); + mUpdater->mTransparency = state.mMoonAlpha; + mUpdater->mShadowBlend = state.mShadowBlend; + } + + void Moon::setAtmosphereColor(const osg::Vec4f& color) + { + mUpdater->mAtmosphereColor = color; + } + + void Moon::setColor(const osg::Vec4f& color) + { + mUpdater->mMoonColor = color; + } + + unsigned int Moon::getPhaseInt() const + { + switch (mPhase) + { + case MoonState::Phase::New: + return 0; + case MoonState::Phase::WaxingCrescent: + return 1; + case MoonState::Phase::WaningCrescent: + return 1; + case MoonState::Phase::FirstQuarter: + return 2; + case MoonState::Phase::ThirdQuarter: + return 2; + case MoonState::Phase::WaxingGibbous: + return 3; + case MoonState::Phase::WaningGibbous: + return 3; + case MoonState::Phase::Full: + return 4; + default: + return 0; + } + } + + void Moon::setPhase(const MoonState::Phase& phase) + { + if(mPhase == phase) + return; + + mPhase = phase; + + std::string textureName = "textures/tx_"; + + if (mType == Moon::Type_Secunda) + textureName += "secunda_"; + else + textureName += "masser_"; + + switch (mPhase) + { + case MoonState::Phase::New: + textureName += "new"; + break; + case MoonState::Phase::WaxingCrescent: + textureName += "one_wax"; + break; + case MoonState::Phase::FirstQuarter: + textureName += "half_wax"; + break; + case MoonState::Phase::WaxingGibbous: + textureName += "three_wax"; + break; + case MoonState::Phase::WaningCrescent: + textureName += "one_wan"; + break; + case MoonState::Phase::ThirdQuarter: + textureName += "half_wan"; + break; + case MoonState::Phase::WaningGibbous: + textureName += "three_wan"; + break; + case MoonState::Phase::Full: + textureName += "full"; + break; + default: + break; + } + + textureName += ".dds"; + + if (mType == Moon::Type_Secunda) + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); + else + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); + } + + int RainCounter::numParticlesToCreate(double dt) const + { + // limit dt to avoid large particle emissions if there are jumps in the simulation time + // 0.2 seconds is the same cap as used in Engine's frame loop + dt = std::min(dt, 0.2); + return ConstantRateCounter::numParticlesToCreate(dt); + } + + RainShooter::RainShooter() + : mAngle(0.f) + { } + + void RainShooter::shoot(osgParticle::Particle* particle) const + { + particle->setVelocity(mVelocity); + particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); + } + + void RainShooter::setVelocity(const osg::Vec3f& velocity) + { + mVelocity = velocity; + } + + void RainShooter::setAngle(float angle) + { + mAngle = angle; + } + + osg::Object* RainShooter::cloneType() const + { + return new RainShooter; + } + + osg::Object* RainShooter::clone(const osg::CopyOp &) const + { + return new RainShooter(*this); + } + + ModVertexAlphaVisitor::ModVertexAlphaVisitor(ModVertexAlphaVisitor::MeshType type) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mType(type) + { } + + void ModVertexAlphaVisitor::apply(osg::Geometry& geometry) + { + osg::ref_ptr colors = new osg::Vec4Array(geometry.getVertexArray()->getNumElements()); + for (unsigned int i=0; isize(); ++i) + { + float alpha = 1.f; + + switch (mType) + { + case ModVertexAlphaVisitor::Atmosphere: + { + // this is a cylinder, so every second vertex belongs to the bottom-most row + alpha = (i%2) ? 0.f : 1.f; + break; + } + case ModVertexAlphaVisitor::Clouds: + { + if (i>= 49 && i <= 64) + alpha = 0.f; // bottom-most row + else if (i>= 33 && i <= 48) + alpha = 0.25098; // second row + else + alpha = 1.f; + break; + } + case ModVertexAlphaVisitor::Stars: + { + if (geometry.getColorArray()) + { + osg::Vec4Array* origColors = static_cast(geometry.getColorArray()); + alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; + } + else + alpha = 1.f; + break; + } + } + + (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); + } + + geometry.setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } +} diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp new file mode 100644 index 0000000000..ae04c88d23 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.hpp @@ -0,0 +1,343 @@ +#ifndef OPENMW_MWRENDER_SKYUTIL_H +#define OPENMW_MWRENDER_SKYUTIL_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Resource +{ + class ImageManager; + class SceneManager; +} + +namespace MWRender +{ + struct MoonUpdater; + class SunUpdater; + class SunFlashCallback; + class SunGlareCallback; + + struct WeatherResult + { + std::string mCloudTexture; + std::string mNextCloudTexture; + float mCloudBlendFactor; + + osg::Vec4f mFogColor; + + osg::Vec4f mAmbientColor; + + osg::Vec4f mSkyColor; + + // sun light color + osg::Vec4f mSunColor; + + // alpha is the sun transparency + osg::Vec4f mSunDiscColor; + + float mFogDepth; + + float mDLFogFactor; + float mDLFogOffset; + + float mWindSpeed; + float mBaseWindSpeed; + float mCurrentWindSpeed; + float mNextWindSpeed; + + float mCloudSpeed; + + float mGlareView; + + bool mNight; // use night skybox + float mNightFade; // fading factor for night skybox + + bool mIsStorm; + + std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; + + std::string mParticleEffect; + std::string mRainEffect; + float mPrecipitationAlpha; + + float mRainDiameter; + float mRainMinHeight; + float mRainMaxHeight; + float mRainSpeed; + float mRainEntranceSpeed; + int mRainMaxRaindrops; + + osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; + }; + + struct MoonState + { + enum class Phase + { + Full, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + New, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + Unspecified + }; + + float mRotationFromHorizon; + float mRotationFromNorth; + Phase mPhase; + float mShadowBlend; + float mMoonAlpha; + }; + + osg::ref_ptr createAlphaTrackingUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(); + + class OcclusionCallback + { + public: + OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal); + + protected: + float getVisibleRatio (osg::Camera* camera); + + private: + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + + std::map, float> mLastRatio; + }; + + class AtmosphereUpdater : public SceneUtil::StateSetUpdater + { + public: + void setEmissionColor(const osg::Vec4f& emissionColor); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mEmissionColor; + }; + + class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater + { + public: + AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders); + + void setFade(float fade); + + protected: + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mColor; + osg::ref_ptr mTexture; + bool mForceShaders; + }; + + class CloudUpdater : public SceneUtil::StateSetUpdater + { + public: + CloudUpdater(bool forceShaders); + + void setTexture(osg::ref_ptr texture); + + void setEmissionColor(const osg::Vec4f& emissionColor); + void setOpacity(float opacity); + void setTextureCoord(float timer); + + protected: + void setDefaults(osg::StateSet *stateset) override; + void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; + + private: + osg::ref_ptr mTexture; + osg::Vec4f mEmissionColor; + float mOpacity; + bool mForceShaders; + osg::Matrixf mTexMat; + }; + + /// Transform that removes the eyepoint of the modelview matrix, + /// i.e. its children are positioned relative to the camera. + class CameraRelativeTransform : public osg::Transform + { + public: + CameraRelativeTransform(); + + CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop); + + META_Node(MWRender, CameraRelativeTransform) + + const osg::Vec3f& getLastViewPoint() const; + + bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override; + + osg::BoundingSphere computeBound() const override; + + private: + // viewPoint for the current frame + mutable osg::Vec3f mViewPoint; + }; + + /// @brief Hides the node subgraph if the eye point is below water. + /// @note Must be added as cull callback. + /// @note Meant to be used on a node that is child of a CameraRelativeTransform. + /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. + class UnderwaterSwitchCallback : public SceneUtil::NodeCallback + { + public: + UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform); + bool isUnderwater(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv); + void setEnabled(bool enabled); + void setWaterLevel(float waterLevel); + + private: + osg::ref_ptr mCameraRelativeTransform; + bool mEnabled; + float mWaterLevel; + }; + + /// A base class for the sun and moons. + class CelestialBody + { + public: + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u); + + virtual ~CelestialBody() = default; + + virtual void adjustTransparency(const float ratio) = 0; + + void setVisible(bool visible); + + protected: + unsigned int mVisibleMask; + static const float mDistance; + osg::ref_ptr mTransform; + osg::ref_ptr mGeom; + }; + + class Sun : public CelestialBody + { + public: + Sun(osg::Group* parentNode, Resource::ImageManager& imageManager); + + ~Sun(); + + void setColor(const osg::Vec4f& color); + void adjustTransparency(const float ratio) override; + + void setDirection(const osg::Vec3f& direction); + void setGlareTimeOfDayFade(float val); + + private: + /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. + osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible); + + void createSunFlash(Resource::ImageManager& imageManager); + void destroySunFlash(); + + void createSunGlare(); + void destroySunGlare(); + + osg::ref_ptr mUpdater; + osg::ref_ptr mSunFlashNode; + osg::ref_ptr mSunGlareNode; + osg::ref_ptr mSunFlashCallback; + osg::ref_ptr mSunGlareCallback; + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + }; + + class Moon : public CelestialBody + { + public: + enum Type + { + Type_Masser = 0, + Type_Secunda + }; + + Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type); + + ~Moon(); + + void adjustTransparency(const float ratio) override; + void setState(const MoonState state); + void setAtmosphereColor(const osg::Vec4f& color); + void setColor(const osg::Vec4f& color); + + unsigned int getPhaseInt() const; + + private: + Type mType; + MoonState::Phase mPhase; + osg::ref_ptr mUpdater; + + void setPhase(const MoonState::Phase& phase); + }; + + class RainCounter : public osgParticle::ConstantRateCounter + { + public: + int numParticlesToCreate(double dt) const override; + }; + + class RainShooter : public osgParticle::Shooter + { + public: + RainShooter(); + + osg::Object* cloneType() const override; + + osg::Object* clone(const osg::CopyOp &) const override; + + void shoot(osgParticle::Particle* particle) const override; + + void setVelocity(const osg::Vec3f& velocity); + void setAngle(float angle); + + private: + osg::Vec3f mVelocity; + float mAngle; + }; + + class ModVertexAlphaVisitor : public osg::NodeVisitor + { + public: + enum MeshType + { + Atmosphere, + Stars, + Clouds + }; + + ModVertexAlphaVisitor(MeshType type); + + void apply(osg::Geometry& geometry) override; + + private: + MeshType mType; + }; +} + +#endif diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 4bdd784db1..9055b95ee1 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -22,8 +22,6 @@ #include -using namespace MWWorld; - namespace { static const int invalidWeatherID = -1; @@ -38,1226 +36,1241 @@ namespace { return x * (1-factor) + y * factor; } -} -template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const -{ - WeatherSetting setting = timeSettings.getSetting(prefix); - float preSunriseTime = setting.mPreSunriseTime; - float postSunriseTime = setting.mPostSunriseTime; - float preSunsetTime = setting.mPreSunsetTime; - float postSunsetTime = setting.mPostSunsetTime; - - // night - if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) - return mNightValue; - // sunrise - else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) + osg::Vec3f calculateStormDirection(const std::string& particleEffect) { - float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; - float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - if (gameHour <= middle) + osg::Vec3f stormDirection = MWWorld::Weather::defaultDirection(); + if (particleEffect == "meshes\\ashcloud.nif" || particleEffect == "meshes\\blightcloud.nif") { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mNightValue, factor); - } - else - { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mDayValue, factor); + osg::Vec3f playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); + playerPos.z() = 0; + osg::Vec3f redMountainPos = osg::Vec3f(25000.f, 70000.f, 0.f); + stormDirection = (playerPos - redMountainPos); + stormDirection.normalize(); } + return stormDirection; } - // day - else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) - return mDayValue; - // sunset - else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) +} + +namespace MWWorld +{ + template + T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { - float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; - float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; - if (gameHour <= middle) + // night + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) + return mNightValue; + // sunrise + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mDayValue, factor); - } - else - { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mNightValue, factor); - } - } - // shut up compiler - return T(); -} + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - -template class MWWorld::TimeOfDayInterpolator; -template class MWWorld::TimeOfDayInterpolator; - -Weather::Weather(const std::string& name, - float stormWindSpeed, - float rainSpeed, - float dlFactor, - float dlOffset, - const std::string& particleEffect) - : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) - , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) - , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) - , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) - , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) - , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) - , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) - , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) - , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) - , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) - , mIsStorm(mWindSpeed > stormWindSpeed) - , mRainSpeed(rainSpeed) - , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) - , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) - , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) - , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) - , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) - , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) - , mParticleEffect(particleEffect) - , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") - , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) - , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) - , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) - , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() - , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) - , mFlashBrightness(0.0f) -{ - mDL.FogFactor = dlFactor; - mDL.FogOffset = dlOffset; - mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); - mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); - mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); - mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); - - // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. - - if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect - { - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); - if (mAmbientLoopSoundID.empty()) // default to "rain" if not set - mAmbientLoopSoundID = "rain"; - } - else - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); - - if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) - mAmbientLoopSoundID.clear(); -} - -float Weather::transitionDelta() const -{ - // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the - // measurement is in real time, not in-game time. - return mTransitionDelta; -} - -float Weather::cloudBlendFactor(const float transitionRatio) const -{ - // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. - return transitionRatio / mCloudsMaximumPercent; -} - -float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) -{ - // When paused, the flash brightness remains the same and no new strikes can occur. - if(!isPaused) - { - // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. - if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) - { - flashDecrement(elapsedSeconds); - - if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) + if (gameHour <= middle) { - lightningAndThunder(); + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mNightValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mDayValue, factor); + } + } + // day + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) + return mDayValue; + // sunset + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) + { + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mDayValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mNightValue, factor); + } + } + // shut up compiler + return T(); + } + + template class MWWorld::TimeOfDayInterpolator; + template class MWWorld::TimeOfDayInterpolator; + + osg::Vec3f Weather::defaultDirection() + { + static const osg::Vec3f direction = osg::Vec3f(0.f, 1.f, 0.f); + return direction; + } + + Weather::Weather(const std::string& name, + float stormWindSpeed, + float rainSpeed, + float dlFactor, + float dlOffset, + const std::string& particleEffect) + : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) + , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) + , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) + , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) + , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) + , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) + , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) + , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) + , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) + , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) + , mIsStorm(mWindSpeed > stormWindSpeed) + , mRainSpeed(rainSpeed) + , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) + , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) + , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) + , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) + , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) + , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) + , mParticleEffect(particleEffect) + , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") + , mStormDirection(Weather::defaultDirection()) + , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) + , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) + , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) + , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) + , mThunderSoundID() + , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) + , mFlashBrightness(0.0f) + { + mDL.FogFactor = dlFactor; + mDL.FogOffset = dlOffset; + mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); + mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); + mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); + mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); + + // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. + + if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect + { + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); + if (mAmbientLoopSoundID.empty()) // default to "rain" if not set + mAmbientLoopSoundID = "rain"; + } + else + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); + + if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) + mAmbientLoopSoundID.clear(); + } + + float Weather::transitionDelta() const + { + // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the + // measurement is in real time, not in-game time. + return mTransitionDelta; + } + + float Weather::cloudBlendFactor(const float transitionRatio) const + { + // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. + return transitionRatio / mCloudsMaximumPercent; + } + + float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) + { + // When paused, the flash brightness remains the same and no new strikes can occur. + if(!isPaused) + { + // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. + if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + { + flashDecrement(elapsedSeconds); + + if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) + { + lightningAndThunder(); + } + } + else + { + mFlashBrightness = 0.0f; + } + } + + return mFlashBrightness; + } + + inline void Weather::flashDecrement(const float elapsedSeconds) + { + // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was + // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). + float decrement = mFlashDecrement * elapsedSeconds; + mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; + } + + inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const + { + // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes + // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of + // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to + // scaled based on how far past it is past the Thunder Threshold. + float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); + return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; + } + + inline void Weather::lightningAndThunder(void) + { + // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. + // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance + // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. + // TODO: Determine the distribution of each distance to see if it's evenly weighted. + unsigned int distance = Misc::Rng::rollDice(4); + // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. + mFlashBrightness += 1 - (distance * 0.25f); + MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); + } + + RegionWeather::RegionWeather(const ESM::Region& region) + : mWeather(invalidWeatherID) + , mChances() + { + mChances.reserve(10); + mChances.push_back(region.mData.mClear); + mChances.push_back(region.mData.mCloudy); + mChances.push_back(region.mData.mFoggy); + mChances.push_back(region.mData.mOvercast); + mChances.push_back(region.mData.mRain); + mChances.push_back(region.mData.mThunder); + mChances.push_back(region.mData.mAsh); + mChances.push_back(region.mData.mBlight); + mChances.push_back(region.mData.mA); + mChances.push_back(region.mData.mB); + } + + RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) + : mWeather(state.mWeather) + , mChances(state.mChances) + { + } + + RegionWeather::operator ESM::RegionWeatherState() const + { + ESM::RegionWeatherState state = + { + mWeather, + mChances + }; + + return state; + } + + void RegionWeather::setChances(const std::vector& chances) + { + if(mChances.size() < chances.size()) + { + mChances.reserve(chances.size()); + } + + int i = 0; + for(char chance : chances) + { + mChances[i] = chance; + i++; + } + + // Regional weather no longer supports the current type, select a new weather pattern. + if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) + { + chooseNewWeather(); + } + } + + void RegionWeather::setWeather(int weatherID) + { + mWeather = weatherID; + } + + int RegionWeather::getWeather() + { + // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. + // Note that the region weather will be expired periodically when the weather update timer expires. + if(mWeather == invalidWeatherID) + { + chooseNewWeather(); + } + + return mWeather; + } + + void RegionWeather::chooseNewWeather() + { + // All probabilities must add to 100 (responsibility of the user). + // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 + // and 70% will be greater than 30 (in theory). + int chance = Misc::Rng::rollDice(100) + 1; // 1..100 + int sum = 0; + int i = 0; + for(; static_cast(i) < mChances.size(); ++i) + { + sum += mChances[i]; + if(chance <= sum) + { + mWeather = i; + return; + } + } + + // if we hit this path then the chances don't add to 100, choose a default weather instead + mWeather = 0; + } + + MoonModel::MoonModel(const std::string& name) + : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) + , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) + , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) + , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) + , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) + , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) + , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) + , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) + , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) + , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) + { + // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably + // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. + mSpeed = std::min(mSpeed, 180.0f / 23.0f); + } + + MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const + { + float rotationFromHorizon = angle(gameTime); + MWRender::MoonState state = + { + rotationFromHorizon, + mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. + phase(gameTime), + shadowBlend(rotationFromHorizon), + earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) + }; + + return state; + } + + inline float MoonModel::angle(const TimeStamp& gameTime) const + { + // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the + // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. + + // When calculating the angle of the moon, several cases have to be taken into account: + // 1. Moon rises and then sets in one day. + // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). + // 3. Moon sets and then rises in one day. + float moonRiseHourToday = moonRiseHour(gameTime.getDay()); + float moonRiseAngleToday = 0; + + if(gameTime.getHour() < moonRiseHourToday) + { + float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); + if(moonRiseHourYesterday < 24) + { + float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); + if(moonRiseAngleYesterday < 180) + { + // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. + moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + } } } else { - mFlashBrightness = 0.0f; + moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); } + + if(moonRiseAngleToday >= 180) + { + // The moon set today, reset the angle to the horizon. + moonRiseAngleToday = 0; + } + + return moonRiseAngleToday; } - return mFlashBrightness; -} + inline float MoonModel::moonRiseHour(unsigned int daysPassed) const + { + // This arises from the start date of 16 Last Seed, 427 + // TODO: Find an alternate formula that doesn't rely on this day being fixed. + static const unsigned int startDay = 16; -inline void Weather::flashDecrement(const float elapsedSeconds) -{ - // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was - // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). - float decrement = mFlashDecrement * elapsedSeconds; - mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; -} + // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning + // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. + // Note that we don't modulo after adding the latest daily increment because other calculations need to + // know if doing so would cause the moon rise to be postponed until the next day (which happens when + // the moon rise hour is >= 24 in Morrowind). + return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); + } -inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const -{ - // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes - // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of - // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to - // scaled based on how far past it is past the Thunder Threshold. - float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); - return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; -} + inline float MoonModel::rotation(float hours) const + { + // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. + // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure + // of whole rotations that could be completed in a day. + return 15.0f * mSpeed * hours; + } -inline void Weather::lightningAndThunder(void) -{ - // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. - // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance - // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. - // TODO: Determine the distribution of each distance to see if it's evenly weighted. - unsigned int distance = Misc::Rng::rollDice(4); - // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. - mFlashBrightness += 1 - (distance * 0.25f); - MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); -} + MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const + { + // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. -RegionWeather::RegionWeather(const ESM::Region& region) - : mWeather(invalidWeatherID) - , mChances() -{ - mChances.reserve(10); - mChances.push_back(region.mData.mClear); - mChances.push_back(region.mData.mCloudy); - mChances.push_back(region.mData.mFoggy); - mChances.push_back(region.mData.mOvercast); - mChances.push_back(region.mData.mRain); - mChances.push_back(region.mData.mThunder); - mChances.push_back(region.mData.mAsh); - mChances.push_back(region.mData.mBlight); - mChances.push_back(region.mData.mA); - mChances.push_back(region.mData.mB); -} + // If the moon didn't rise yet today, use yesterday's moon phase. + if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) + return static_cast((gameTime.getDay() / 3) % 8); + else + return static_cast(((gameTime.getDay() + 1) / 3) % 8); + } -RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) - : mWeather(state.mWeather) - , mChances(state.mChances) -{ -} + inline float MoonModel::shadowBlend(float angle) const + { + // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk + // that is roughly the color of the sky, to a textured surface. + // Depending on the current angle, the following values describe the ratio between the textured moon + // and the solid disk: + // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 + // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) + // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 + // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) + float fadeAngle = mFadeStartAngle - mFadeEndAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float fadeStartAngle2 = 180.0f - mFadeStartAngle; + if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) + return (angle - mFadeEndAngle) / fadeAngle; + else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) + return 1.0f; + else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) + return (fadeEndAngle2 - angle) / fadeAngle; + else + return 0.0f; + } -RegionWeather::operator ESM::RegionWeatherState() const -{ - ESM::RegionWeatherState state = - { - mWeather, - mChances + inline float MoonModel::hourlyAlpha(float gameHour) const + { + // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon + // appears and disappears. + // Depending on the current hour, the following values describe how transparent the moon is. + // 1. From Fade Out Start to Fade Out Finish: 1..0 + // 2. From Fade Out Finish to Fade In Start: 0 (transparent) + // 3. From Fade In Start to Fade In Finish: 0..1 + // 4. From Fade In Finish to Fade Out Start: 1 (solid) + if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) + return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); + else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) + return 0.0f; + else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) + return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); + else + return 1.0f; + } + + inline float MoonModel::earlyMoonShadowAlpha(float angle) const + { + // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. + // Depending on the current angle, the following values describe how transparent the moon is. + // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 + // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) + // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 + // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) + float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; + if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) + return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; + else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) + return 1.0f; + else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) + return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; + else + return 0.0f; + } + + WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) + : mStore(store) + , mRendering(rendering) + , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) + , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) + , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) + , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) + , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) + , mNightFade(0, 0, 0, 1) + , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) + , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) + , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), + Fallback::Map::getFloat("Water_UnderwaterDayFog"), + Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), + Fallback::Map::getFloat("Water_UnderwaterNightFog")) + , mWeatherSettings() + , mMasser("Masser") + , mSecunda("Secunda") + , mWindSpeed(0.f) + , mCurrentWindSpeed(0.f) + , mNextWindSpeed(0.f) + , mIsStorm(false) + , mPrecipitation(false) + , mStormDirection(Weather::defaultDirection()) + , mCurrentRegion() + , mTimePassed(0) + , mFastForward(false) + , mWeatherUpdateTime(mHoursBetweenWeatherChanges) + , mTransitionFactor(0) + , mNightDayMode(Default) + , mCurrentWeather(0) + , mNextWeather(0) + , mQueuedWeather(0) + , mRegions() + , mResult() + , mAmbientSound(nullptr) + , mPlayingSoundID() + { + mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; + mTimeSettings.mNightEnd = mSunriseTime; + mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; + mTimeSettings.mDayEnd = mSunsetTime; + + mTimeSettings.addSetting("Sky"); + mTimeSettings.addSetting("Ambient"); + mTimeSettings.addSetting("Fog"); + mTimeSettings.addSetting("Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart }; - return state; -} + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; -void RegionWeather::setChances(const std::vector& chances) -{ - if(mChances.size() < chances.size()) - { - mChances.reserve(chances.size()); - } + mWeatherSettings.reserve(10); + // These distant land fog factor and offset values are the defaults MGE XE provides. Should be + // provided by settings somewhere? + addWeather("Clear", 1.0f, 0.0f); // 0 + addWeather("Cloudy", 0.9f, 0.0f); // 1 + addWeather("Foggy", 0.2f, 30.0f); // 2 + addWeather("Overcast", 0.7f, 0.0f); // 3 + addWeather("Rain", 0.5f, 10.0f); // 4 + addWeather("Thunderstorm", 0.5f, 20.0f); // 5 + addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 + addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 + addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 + addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - int i = 0; - for(char chance : chances) - { - mChances[i] = chance; - i++; - } - - // Regional weather no longer supports the current type, select a new weather pattern. - if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) - { - chooseNewWeather(); - } -} - -void RegionWeather::setWeather(int weatherID) -{ - mWeather = weatherID; -} - -int RegionWeather::getWeather() -{ - // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. - // Note that the region weather will be expired periodically when the weather update timer expires. - if(mWeather == invalidWeatherID) - { - chooseNewWeather(); - } - - return mWeather; -} - -void RegionWeather::chooseNewWeather() -{ - // All probabilities must add to 100 (responsibility of the user). - // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 - // and 70% will be greater than 30 (in theory). - int chance = Misc::Rng::rollDice(100) + 1; // 1..100 - int sum = 0; - int i = 0; - for(; static_cast(i) < mChances.size(); ++i) - { - sum += mChances[i]; - if(chance <= sum) + Store::iterator it = store.get().begin(); + for(; it != store.get().end(); ++it) { - mWeather = i; - return; + std::string regionID = Misc::StringUtils::lowerCase(it->mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } + + forceWeather(0); } - // if we hit this path then the chances don't add to 100, choose a default weather instead - mWeather = 0; -} - -MoonModel::MoonModel(const std::string& name) - : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) - , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) - , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) - , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) - , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) - , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) - , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) - , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) - , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) - , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) -{ - // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably - // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. - mSpeed = std::min(mSpeed, 180.0f / 23.0f); -} - -MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const -{ - float rotationFromHorizon = angle(gameTime); - MWRender::MoonState state = - { - rotationFromHorizon, - mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. - phase(gameTime), - shadowBlend(rotationFromHorizon), - earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) - }; - - return state; -} - -inline float MoonModel::angle(const TimeStamp& gameTime) const -{ - // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the - // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. - - // When calculating the angle of the moon, several cases have to be taken into account: - // 1. Moon rises and then sets in one day. - // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). - // 3. Moon sets and then rises in one day. - float moonRiseHourToday = moonRiseHour(gameTime.getDay()); - float moonRiseAngleToday = 0; - - if(gameTime.getHour() < moonRiseHourToday) + WeatherManager::~WeatherManager() { - float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); - if(moonRiseHourYesterday < 24) + stopSounds(); + } + + void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) + { + // In Morrowind, this seems to have the following behavior, when applied to the current region: + // - When there is no transition in progress, start transitioning to the new weather. + // - If there is a transition in progress, queue up the transition and process it when the current one completes. + // - If there is a transition in progress, and a queued transition, overwrite the queued transition. + // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, + // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. + // If the region isn't current, Morrowind will store the new weather for the region in question. + + if(weatherID < mWeatherSettings.size()) { - float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); - if(moonRiseAngleYesterday < 180) + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); + if(it != mRegions.end()) { - // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. - moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + it->second.setWeather(weatherID); + regionalWeatherChanged(it->first, it->second); } } } - else + + void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { - moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); - } + // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. + // In Morrowind, this seems to have the following behavior when applied to the current region: + // - If the region supports the current weather, no change in current weather occurs. + // - If the region no longer supports the current weather, and there is no transition in progress, begin to + // transition to a new supported weather type. + // - If the region no longer supports the current weather, and there is a transition in progress, queue a + // transition to a new supported weather type. - if(moonRiseAngleToday >= 180) - { - // The moon set today, reset the angle to the horizon. - moonRiseAngleToday = 0; - } - - return moonRiseAngleToday; -} - -inline float MoonModel::moonRiseHour(unsigned int daysPassed) const -{ - // This arises from the start date of 16 Last Seed, 427 - // TODO: Find an alternate formula that doesn't rely on this day being fixed. - static const unsigned int startDay = 16; - - // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning - // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. - // Note that we don't modulo after adding the latest daily increment because other calculations need to - // know if doing so would cause the moon rise to be postponed until the next day (which happens when - // the moon rise hour is >= 24 in Morrowind). - return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); -} - -inline float MoonModel::rotation(float hours) const -{ - // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. - // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure - // of whole rotations that could be completed in a day. - return 15.0f * mSpeed * hours; -} - -MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const -{ - // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. - - // If the moon didn't rise yet today, use yesterday's moon phase. - if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) - return static_cast((gameTime.getDay() / 3) % 8); - else - return static_cast(((gameTime.getDay() + 1) / 3) % 8); -} - -inline float MoonModel::shadowBlend(float angle) const -{ - // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk - // that is roughly the color of the sky, to a textured surface. - // Depending on the current angle, the following values describe the ratio between the textured moon - // and the solid disk: - // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 - // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) - // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 - // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) - float fadeAngle = mFadeStartAngle - mFadeEndAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float fadeStartAngle2 = 180.0f - mFadeStartAngle; - if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) - return (angle - mFadeEndAngle) / fadeAngle; - else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) - return 1.0f; - else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) - return (fadeEndAngle2 - angle) / fadeAngle; - else - return 0.0f; -} - -inline float MoonModel::hourlyAlpha(float gameHour) const -{ - // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon - // appears and disappears. - // Depending on the current hour, the following values describe how transparent the moon is. - // 1. From Fade Out Start to Fade Out Finish: 1..0 - // 2. From Fade Out Finish to Fade In Start: 0 (transparent) - // 3. From Fade In Start to Fade In Finish: 0..1 - // 4. From Fade In Finish to Fade Out Start: 1 (solid) - if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) - return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); - else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) - return 0.0f; - else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) - return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); - else - return 1.0f; -} - -inline float MoonModel::earlyMoonShadowAlpha(float angle) const -{ - // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. - // Depending on the current angle, the following values describe how transparent the moon is. - // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 - // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) - // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 - // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) - float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; - if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) - return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; - else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) - return 1.0f; - else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) - return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; - else - return 0.0f; -} - -WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) - : mStore(store) - , mRendering(rendering) - , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) - , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) - , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) - , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) - , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) - , mNightFade(0, 0, 0, 1) - , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) - , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) - , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), - Fallback::Map::getFloat("Water_UnderwaterDayFog"), - Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), - Fallback::Map::getFloat("Water_UnderwaterNightFog")) - , mWeatherSettings() - , mMasser("Masser") - , mSecunda("Secunda") - , mWindSpeed(0.f) - , mCurrentWindSpeed(0.f) - , mNextWindSpeed(0.f) - , mIsStorm(false) - , mPrecipitation(false) - , mStormDirection(0,1,0) - , mCurrentRegion() - , mTimePassed(0) - , mFastForward(false) - , mWeatherUpdateTime(mHoursBetweenWeatherChanges) - , mTransitionFactor(0) - , mNightDayMode(Default) - , mCurrentWeather(0) - , mNextWeather(0) - , mQueuedWeather(0) - , mRegions() - , mResult() - , mAmbientSound(nullptr) - , mPlayingSoundID() -{ - mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime; - mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; - mTimeSettings.mDayEnd = mSunsetTime; - - mTimeSettings.addSetting("Sky"); - mTimeSettings.addSetting("Ambient"); - mTimeSettings.addSetting("Fog"); - mTimeSettings.addSetting("Sun"); - - // Morrowind handles stars settings differently for other ones - mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); - mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); - mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); - - WeatherSetting starSetting = { - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsPostSunsetStart, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart - }; - - mTimeSettings.mSunriseTransitions["Stars"] = starSetting; - - mWeatherSettings.reserve(10); - // These distant land fog factor and offset values are the defaults MGE XE provides. Should be - // provided by settings somewhere? - addWeather("Clear", 1.0f, 0.0f); // 0 - addWeather("Cloudy", 0.9f, 0.0f); // 1 - addWeather("Foggy", 0.2f, 30.0f); // 2 - addWeather("Overcast", 0.7f, 0.0f); // 3 - addWeather("Rain", 0.5f, 10.0f); // 4 - addWeather("Thunderstorm", 0.5f, 20.0f); // 5 - addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 - addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 - addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 - addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - - Store::iterator it = store.get().begin(); - for(; it != store.get().end(); ++it) - { - std::string regionID = Misc::StringUtils::lowerCase(it->mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); - } - - forceWeather(0); -} - -WeatherManager::~WeatherManager() -{ - stopSounds(); -} - -void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) -{ - // In Morrowind, this seems to have the following behavior, when applied to the current region: - // - When there is no transition in progress, start transitioning to the new weather. - // - If there is a transition in progress, queue up the transition and process it when the current one completes. - // - If there is a transition in progress, and a queued transition, overwrite the queued transition. - // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, - // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. - // If the region isn't current, Morrowind will store the new weather for the region in question. - - if(weatherID < mWeatherSettings.size()) - { std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { - it->second.setWeather(weatherID); + it->second.setChances(chances); regionalWeatherChanged(it->first, it->second); } } -} -void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) -{ - // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. - // In Morrowind, this seems to have the following behavior when applied to the current region: - // - If the region supports the current weather, no change in current weather occurs. - // - If the region no longer supports the current weather, and there is no transition in progress, begin to - // transition to a new supported weather type. - // - If the region no longer supports the current weather, and there is a transition in progress, queue a - // transition to a new supported weather type. - - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) + void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { - it->second.setChances(chances); - regionalWeatherChanged(it->first, it->second); - } -} - -void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) -{ - // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to - // be changed immediately, and any transitions for the previous region discarded. - { - std::map::iterator it = mRegions.find(playerRegion); - if(it != mRegions.end() && playerRegion != mCurrentRegion) + // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to + // be changed immediately, and any transitions for the previous region discarded. { - mCurrentRegion = playerRegion; - forceWeather(it->second.getWeather()); - } - } -} - -float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) -{ - float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); - if (currentSpeed == 0.f) - currentSpeed = targetSpeed; - - float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; - float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; - - if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) - currentSpeed = updatedSpeed; - - return currentSpeed; -} - -void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) -{ - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - - if(!paused || mFastForward) - { - // Add new transitions when either the player's current external region changes. - std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); - if(updateWeatherTime() || updateWeatherRegion(playerRegion)) - { - std::map::iterator it = mRegions.find(mCurrentRegion); - if(it != mRegions.end()) + std::map::iterator it = mRegions.find(playerRegion); + if(it != mRegions.end() && playerRegion != mCurrentRegion) { - addWeatherTransition(it->second.getWeather()); + mCurrentRegion = playerRegion; + forceWeather(it->second.getWeather()); } } - - updateWeatherTransitions(duration); } - bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; - if (isExterior && !isDay) - mNightDayMode = ExteriorNight; - else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) - mNightDayMode = InteriorDay; - else - mNightDayMode = Default; - - if(!isExterior) + float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) { - mRendering.setSkyEnabled(false); - stopSounds(); - mWindSpeed = 0.f; - mCurrentWindSpeed = 0.f; - mNextWindSpeed = 0.f; - return; + float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); + if (currentSpeed == 0.f) + currentSpeed = targetSpeed; + + float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; + float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; + + if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) + currentSpeed = updatedSpeed; + + return currentSpeed; } - calculateWeatherResult(time.getHour(), duration, paused); - - if (!paused) + void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { - mWindSpeed = mResult.mWindSpeed; - mCurrentWindSpeed = mResult.mCurrentWindSpeed; - mNextWindSpeed = mResult.mNextWindSpeed; - } + MWWorld::ConstPtr player = MWMechanics::getPlayer(); - mIsStorm = mResult.mIsStorm; - - // For some reason Ash Storm is not considered as a precipitation weather in game - mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) - && mResult.mParticleEffect != "meshes\\ashcloud.nif"; - - if (mIsStorm) - { - osg::Vec3f stormDirection(0, 1, 0); - if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") + if(!paused || mFastForward) { - osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); - playerPos.z() = 0; - osg::Vec3f redMountainPos (25000, 70000, 0); - stormDirection = (playerPos - redMountainPos); - stormDirection.normalize(); - } - mStormDirection = stormDirection; - mRendering.getSkyManager()->setStormDirection(mStormDirection); - } - - // disable sun during night - if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) - mRendering.getSkyManager()->sunDisable(); - else - mRendering.getSkyManager()->sunEnable(); - - // Update the sun direction. Run it east to west at a fixed angle from overhead. - // The sun's speed at day and night may differ, since mSunriseTime and mNightStart - // mark when the sun is level with the horizon. - { - // Shift times into a 24-hour window beginning at mSunriseTime... - float adjustedHour = time.getHour(); - float adjustedNightStart = mTimeSettings.mNightStart; - if ( time.getHour() < mSunriseTime ) - adjustedHour += 24.f; - if ( mTimeSettings.mNightStart < mSunriseTime ) - adjustedNightStart += 24.f; - - const bool is_night = adjustedHour >= adjustedNightStart; - const float dayDuration = adjustedNightStart - mSunriseTime; - const float nightDuration = 24.f - dayDuration; - - double theta; - if ( !is_night ) - { - theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; - } - else - { - theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; - } - - osg::Vec3f final( - static_cast(cos(theta)), - -0.268f, // approx tan( -15 degrees ) - static_cast(sin(theta))); - mRendering.setSunDirection( final * -1 ); - } - - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); - - float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; - float glareFade = 1.f; - if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) - glareFade = 0.f; - else if (time.getHour() < peakHour) - glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); - else - glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); - - mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); - - mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); - mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); - - mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, - mResult.mDLFogOffset/100.0f, mResult.mFogColor); - mRendering.setAmbientColour(mResult.mAmbientColor); - mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); - - mRendering.getSkyManager()->setWeather(mResult); - - // Play sounds - if (mPlayingSoundID != mResult.mAmbientLoopSoundID) - { - stopSounds(); - if (!mResult.mAmbientLoopSoundID.empty()) - mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( - mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, - MWSound::Type::Sfx, MWSound::PlayMode::Loop - ); - mPlayingSoundID = mResult.mAmbientLoopSoundID; - } - else if (mAmbientSound) - mAmbientSound->setVolume(mResult.mAmbientSoundVolume); -} - -void WeatherManager::stopSounds() -{ - if (mAmbientSound) - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); - mAmbientSound = nullptr; - mPlayingSoundID.clear(); -} - -float WeatherManager::getWindSpeed() const -{ - return mWindSpeed; -} - -bool WeatherManager::isInStorm() const -{ - return mIsStorm; -} - -osg::Vec3f WeatherManager::getStormDirection() const -{ - return mStormDirection; -} - -void WeatherManager::advanceTime(double hours, bool incremental) -{ - // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are - // immediately applied, regardless of whatever transition time might have been remaining. - mTimePassed += hours; - mFastForward = !incremental ? true : mFastForward; -} - -unsigned int WeatherManager::getWeatherID() const -{ - return mCurrentWeather; -} - -NightDayMode WeatherManager::getNightDayMode() const -{ - return mNightDayMode; -} - -bool WeatherManager::useTorches(float hour) const -{ - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; - - return isDark && !mPrecipitation; -} - -void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) -{ - ESM::WeatherState state; - state.mCurrentRegion = mCurrentRegion; - state.mTimePassed = mTimePassed; - state.mFastForward = mFastForward; - state.mWeatherUpdateTime = mWeatherUpdateTime; - state.mTransitionFactor = mTransitionFactor; - state.mCurrentWeather = mCurrentWeather; - state.mNextWeather = mNextWeather; - state.mQueuedWeather = mQueuedWeather; - - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) - { - state.mRegions.insert(std::make_pair(it->first, it->second)); - } - - writer.startRecord(ESM::REC_WTHR); - state.save(writer); - writer.endRecord(ESM::REC_WTHR); -} - -bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) -{ - if(ESM::REC_WTHR == type) - { - static const int oldestCompatibleSaveFormat = 2; - if(reader.getFormat() < oldestCompatibleSaveFormat) - { - // Weather state isn't really all that important, so to preserve older save games, we'll just discard the - // older weather records, rather than fail to handle the record. - reader.skipRecord(); - } - else - { - ESM::WeatherState state; - state.load(reader); - - mCurrentRegion.swap(state.mCurrentRegion); - mTimePassed = state.mTimePassed; - mFastForward = state.mFastForward; - mWeatherUpdateTime = state.mWeatherUpdateTime; - mTransitionFactor = state.mTransitionFactor; - mCurrentWeather = state.mCurrentWeather; - mNextWeather = state.mNextWeather; - mQueuedWeather = state.mQueuedWeather; - - mRegions.clear(); - importRegions(); - - for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + // Add new transitions when either the player's current external region changes. + std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); + if(updateWeatherTime() || updateWeatherRegion(playerRegion)) { - std::map::iterator found = mRegions.find(it->first); - if (found != mRegions.end()) + std::map::iterator it = mRegions.find(mCurrentRegion); + if(it != mRegions.end()) { - found->second = RegionWeather(it->second); + addWeatherTransition(it->second.getWeather()); } } + + updateWeatherTransitions(duration); } - return true; - } + bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; + if (isExterior && !isDay) + mNightDayMode = ExteriorNight; + else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) + mNightDayMode = InteriorDay; + else + mNightDayMode = Default; - return false; -} - -void WeatherManager::clear() -{ - stopSounds(); - - mCurrentRegion = ""; - mTimePassed = 0.0f; - mWeatherUpdateTime = 0.0f; - forceWeather(0); - mRegions.clear(); - importRegions(); -} - -inline void WeatherManager::addWeather(const std::string& name, - float dlFactor, float dlOffset, - const std::string& particleEffect) -{ - static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); - - Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); - - mWeatherSettings.push_back(weather); -} - -inline void WeatherManager::importRegions() -{ - for(const ESM::Region& region : mStore.get()) - { - std::string regionID = Misc::StringUtils::lowerCase(region.mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(region))); - } -} - -inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) -{ - // If the region is current, then add a weather transition for it. - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - if(player.isInCell()) - { - if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + if(!isExterior) { - addWeatherTransition(region.getWeather()); - } - } -} - -inline bool WeatherManager::updateWeatherTime() -{ - mWeatherUpdateTime -= mTimePassed; - mTimePassed = 0.0f; - if(mWeatherUpdateTime <= 0.0f) - { - // Expire all regional weather, so that any call to getWeather() will return a new weather ID. - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) - { - it->second.setWeather(invalidWeatherID); + mRendering.setSkyEnabled(false); + stopSounds(); + mWindSpeed = 0.f; + mCurrentWindSpeed = 0.f; + mNextWindSpeed = 0.f; + return; } - mWeatherUpdateTime += mHoursBetweenWeatherChanges; + calculateWeatherResult(time.getHour(), duration, paused); - return true; - } - - return false; -} - -inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) -{ - if(!playerRegion.empty() && playerRegion != mCurrentRegion) - { - mCurrentRegion = playerRegion; - - return true; - } - - return false; -} - -inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) -{ - // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last - // weather type set, regardless of the remaining transition time. - if(!mFastForward && inTransition()) - { - const float delta = mWeatherSettings[mNextWeather].transitionDelta(); - mTransitionFactor -= elapsedRealSeconds * delta; - if(mTransitionFactor <= 0.0f) + if (!paused) { - mCurrentWeather = mNextWeather; - mNextWeather = mQueuedWeather; - mQueuedWeather = invalidWeatherID; + mWindSpeed = mResult.mWindSpeed; + mCurrentWindSpeed = mResult.mCurrentWindSpeed; + mNextWindSpeed = mResult.mNextWindSpeed; + } - // We may have begun processing the queued transition, so we need to apply the remaining time towards it. - if(inTransition()) + mIsStorm = mResult.mIsStorm; + + // For some reason Ash Storm is not considered as a precipitation weather in game + mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) + && mResult.mParticleEffect != "meshes\\ashcloud.nif"; + + mStormDirection = calculateStormDirection(mResult.mParticleEffect); + mRendering.getSkyManager()->setStormParticleDirection(mStormDirection); + + // disable sun during night + if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) + mRendering.getSkyManager()->sunDisable(); + else + mRendering.getSkyManager()->sunEnable(); + + // Update the sun direction. Run it east to west at a fixed angle from overhead. + // The sun's speed at day and night may differ, since mSunriseTime and mNightStart + // mark when the sun is level with the horizon. + { + // Shift times into a 24-hour window beginning at mSunriseTime... + float adjustedHour = time.getHour(); + float adjustedNightStart = mTimeSettings.mNightStart; + if ( time.getHour() < mSunriseTime ) + adjustedHour += 24.f; + if ( mTimeSettings.mNightStart < mSunriseTime ) + adjustedNightStart += 24.f; + + const bool is_night = adjustedHour >= adjustedNightStart; + const float dayDuration = adjustedNightStart - mSunriseTime; + const float nightDuration = 24.f - dayDuration; + + double theta; + if ( !is_night ) { - const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); - const float remainingSeconds = -(mTransitionFactor / delta); - mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; } else { - mTransitionFactor = 0.0f; + theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + } + + osg::Vec3f final( + static_cast(cos(theta)), + -0.268f, // approx tan( -15 degrees ) + static_cast(sin(theta))); + mRendering.setSunDirection( final * -1 ); + } + + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); + + float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; + float glareFade = 1.f; + if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) + glareFade = 0.f; + else if (time.getHour() < peakHour) + glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); + else + glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); + + mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); + + mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); + mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); + + mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, + mResult.mDLFogOffset/100.0f, mResult.mFogColor); + mRendering.setAmbientColour(mResult.mAmbientColor); + mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); + + mRendering.getSkyManager()->setWeather(mResult); + + // Play sounds + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) + { + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( + mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, + MWSound::Type::Sfx, MWSound::PlayMode::Loop + ); + mPlayingSoundID = mResult.mAmbientLoopSoundID; + } + else if (mAmbientSound) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); + } + + void WeatherManager::stopSounds() + { + if (mAmbientSound) + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound = nullptr; + mPlayingSoundID.clear(); + } + + float WeatherManager::getWindSpeed() const + { + return mWindSpeed; + } + + bool WeatherManager::isInStorm() const + { + return mIsStorm; + } + + osg::Vec3f WeatherManager::getStormDirection() const + { + return mStormDirection; + } + + void WeatherManager::advanceTime(double hours, bool incremental) + { + // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are + // immediately applied, regardless of whatever transition time might have been remaining. + mTimePassed += hours; + mFastForward = !incremental ? true : mFastForward; + } + + unsigned int WeatherManager::getWeatherID() const + { + return mCurrentWeather; + } + + NightDayMode WeatherManager::getNightDayMode() const + { + return mNightDayMode; + } + + bool WeatherManager::useTorches(float hour) const + { + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + + return isDark && !mPrecipitation; + } + + void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) + { + ESM::WeatherState state; + state.mCurrentRegion = mCurrentRegion; + state.mTimePassed = mTimePassed; + state.mFastForward = mFastForward; + state.mWeatherUpdateTime = mWeatherUpdateTime; + state.mTransitionFactor = mTransitionFactor; + state.mCurrentWeather = mCurrentWeather; + state.mNextWeather = mNextWeather; + state.mQueuedWeather = mQueuedWeather; + + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + state.mRegions.insert(std::make_pair(it->first, it->second)); + } + + writer.startRecord(ESM::REC_WTHR); + state.save(writer); + writer.endRecord(ESM::REC_WTHR); + } + + bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if(ESM::REC_WTHR == type) + { + static const int oldestCompatibleSaveFormat = 2; + if(reader.getFormat() < oldestCompatibleSaveFormat) + { + // Weather state isn't really all that important, so to preserve older save games, we'll just discard the + // older weather records, rather than fail to handle the record. + reader.skipRecord(); + } + else + { + ESM::WeatherState state; + state.load(reader); + + mCurrentRegion.swap(state.mCurrentRegion); + mTimePassed = state.mTimePassed; + mFastForward = state.mFastForward; + mWeatherUpdateTime = state.mWeatherUpdateTime; + mTransitionFactor = state.mTransitionFactor; + mCurrentWeather = state.mCurrentWeather; + mNextWeather = state.mNextWeather; + mQueuedWeather = state.mQueuedWeather; + + mRegions.clear(); + importRegions(); + + for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + { + std::map::iterator found = mRegions.find(it->first); + if (found != mRegions.end()) + { + found->second = RegionWeather(it->second); + } + } + } + + return true; + } + + return false; + } + + void WeatherManager::clear() + { + stopSounds(); + + mCurrentRegion = ""; + mTimePassed = 0.0f; + mWeatherUpdateTime = 0.0f; + forceWeather(0); + mRegions.clear(); + importRegions(); + } + + inline void WeatherManager::addWeather(const std::string& name, + float dlFactor, float dlOffset, + const std::string& particleEffect) + { + static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); + + Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); + + mWeatherSettings.push_back(weather); + } + + inline void WeatherManager::importRegions() + { + for(const ESM::Region& region : mStore.get()) + { + std::string regionID = Misc::StringUtils::lowerCase(region.mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + } + } + + inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) + { + // If the region is current, then add a weather transition for it. + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + if(player.isInCell()) + { + if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + { + addWeatherTransition(region.getWeather()); } } } - else + + inline bool WeatherManager::updateWeatherTime() { - if(mQueuedWeather != invalidWeatherID) + mWeatherUpdateTime -= mTimePassed; + mTimePassed = 0.0f; + if(mWeatherUpdateTime <= 0.0f) { - mCurrentWeather = mQueuedWeather; - } - else if(mNextWeather != invalidWeatherID) - { - mCurrentWeather = mNextWeather; + // Expire all regional weather, so that any call to getWeather() will return a new weather ID. + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + it->second.setWeather(invalidWeatherID); + } + + mWeatherUpdateTime += mHoursBetweenWeatherChanges; + + return true; } + return false; + } + + inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) + { + if(!playerRegion.empty() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; + + return true; + } + + return false; + } + + inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) + { + // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last + // weather type set, regardless of the remaining transition time. + if(!mFastForward && inTransition()) + { + const float delta = mWeatherSettings[mNextWeather].transitionDelta(); + mTransitionFactor -= elapsedRealSeconds * delta; + if(mTransitionFactor <= 0.0f) + { + mCurrentWeather = mNextWeather; + mNextWeather = mQueuedWeather; + mQueuedWeather = invalidWeatherID; + + // We may have begun processing the queued transition, so we need to apply the remaining time towards it. + if(inTransition()) + { + const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); + const float remainingSeconds = -(mTransitionFactor / delta); + mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + } + else + { + mTransitionFactor = 0.0f; + } + } + } + else + { + if(mQueuedWeather != invalidWeatherID) + { + mCurrentWeather = mQueuedWeather; + } + else if(mNextWeather != invalidWeatherID) + { + mCurrentWeather = mNextWeather; + } + + mNextWeather = invalidWeatherID; + mQueuedWeather = invalidWeatherID; + mFastForward = false; + } + } + + inline void WeatherManager::forceWeather(const int weatherID) + { + mTransitionFactor = 0.0f; + mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; - mFastForward = false; } -} -inline void WeatherManager::forceWeather(const int weatherID) -{ - mTransitionFactor = 0.0f; - mCurrentWeather = weatherID; - mNextWeather = invalidWeatherID; - mQueuedWeather = invalidWeatherID; -} - -inline bool WeatherManager::inTransition() -{ - return mNextWeather != invalidWeatherID; -} - -inline void WeatherManager::addWeatherTransition(const int weatherID) -{ - // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if - // no transition is in progress, otherwise it queues it to be transitioned. - - assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); - - if(!inTransition() && (weatherID != mCurrentWeather)) + inline bool WeatherManager::inTransition() { - mNextWeather = weatherID; - mTransitionFactor = 1.0f; + return mNextWeather != invalidWeatherID; } - else if(inTransition() && (weatherID != mNextWeather)) + + inline void WeatherManager::addWeatherTransition(const int weatherID) { - mQueuedWeather = weatherID; + // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if + // no transition is in progress, otherwise it queues it to be transitioned. + + assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); + + if(!inTransition() && (weatherID != mCurrentWeather)) + { + mNextWeather = weatherID; + mTransitionFactor = 1.0f; + } + else if(inTransition() && (weatherID != mNextWeather)) + { + mQueuedWeather = weatherID; + } } -} -inline void WeatherManager::calculateWeatherResult(const float gameHour, - const float elapsedSeconds, - const bool isPaused) -{ - float flash = 0.0f; - if(!inTransition()) + inline void WeatherManager::calculateWeatherResult(const float gameHour, + const float elapsedSeconds, + const bool isPaused) { - calculateResult(mCurrentWeather, gameHour); - flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + float flash = 0.0f; + if(!inTransition()) + { + calculateResult(mCurrentWeather, gameHour); + flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + } + else + { + calculateTransitionResult(1 - mTransitionFactor, gameHour); + float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, + elapsedSeconds, + isPaused); + float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, + elapsedSeconds, + isPaused); + flash = currentFlash + nextFlash; + } + osg::Vec4f flashColor(flash, flash, flash, 0.0f); + + mResult.mFogColor += flashColor; + mResult.mAmbientColor += flashColor; + mResult.mSunColor += flashColor; } - else + + inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) { - calculateTransitionResult(1 - mTransitionFactor, gameHour); - float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, - elapsedSeconds, - isPaused); - float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, - elapsedSeconds, - isPaused); - flash = currentFlash + nextFlash; - } - osg::Vec4f flashColor(flash, flash, flash, 0.0f); + const Weather& current = mWeatherSettings[weatherID]; - mResult.mFogColor += flashColor; - mResult.mAmbientColor += flashColor; - mResult.mSunColor += flashColor; -} + mResult.mCloudTexture = current.mCloudTexture; + mResult.mCloudBlendFactor = 0; + mResult.mNextWindSpeed = 0; + mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); + mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; -inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) -{ - const Weather& current = mWeatherSettings[weatherID]; + mResult.mCloudSpeed = current.mCloudSpeed; + mResult.mGlareView = current.mGlareView; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mPrecipitationAlpha = 1.f; - mResult.mCloudTexture = current.mCloudTexture; - mResult.mCloudBlendFactor = 0; - mResult.mNextWindSpeed = 0; - mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); - mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; - - mResult.mCloudSpeed = current.mCloudSpeed; - mResult.mGlareView = current.mGlareView; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; - mResult.mAmbientSoundVolume = 1.f; - mResult.mPrecipitationAlpha = 1.f; - - mResult.mIsStorm = current.mIsStorm; - - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mRainDiameter = current.mRainDiameter; - mResult.mRainMinHeight = current.mRainMinHeight; - mResult.mRainMaxHeight = current.mRainMaxHeight; - mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); - mResult.mDLFogFactor = current.mDL.FogFactor; - mResult.mDLFogOffset = current.mDL.FogOffset; - - WeatherSetting setting = mTimeSettings.getSetting("Sun"); - float preSunsetTime = setting.mPreSunsetTime; - - if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) - { - float factor = 1.f; - if (preSunsetTime > 0) - factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; - factor = std::min(1.f, factor); - mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); - // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because - // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline - // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. - // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. - mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); - for (int i=0; i<3; ++i) - mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); - } - else - mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - - if (gameHour >= mTimeSettings.mDayEnd) - { - // sunset - float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); - fade = fade*fade; - mResult.mSunDiscColor.a() = 1.f - fade; - } - else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) - { - // sunrise - mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; - } - else - mResult.mSunDiscColor.a() = 1; - -} - -inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) -{ - calculateResult(mCurrentWeather, gameHour); - const MWRender::WeatherResult current = mResult; - calculateResult(mNextWeather, gameHour); - const MWRender::WeatherResult other = mResult; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mNextCloudTexture = other.mCloudTexture; - mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); - - mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); - mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); - mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); - - mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); - mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); - mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); - mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); - mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); - - mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); - mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); - mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); - - mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); - mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); - mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); - mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); - - mResult.mNight = current.mNight; - - float threshold = mWeatherSettings[mNextWeather].mRainThreshold; - if (threshold <= 0) - threshold = 0.5f; - - if(factor < threshold) - { mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = 1 - factor / threshold; - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - } - else - { - mResult.mIsStorm = other.mIsStorm; - mResult.mParticleEffect = other.mParticleEffect; - mResult.mRainEffect = other.mRainEffect; - mResult.mRainSpeed = other.mRainSpeed; - mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; - mResult.mRainDiameter = other.mRainDiameter; - mResult.mRainMinHeight = other.mRainMinHeight; - mResult.mRainMaxHeight = other.mRainMaxHeight; - mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); + + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); + mResult.mDLFogFactor = current.mDL.FogFactor; + mResult.mDLFogOffset = current.mDL.FogOffset; + + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) + { + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; + factor = std::min(1.f, factor); + mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); + // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because + // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline + // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. + // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. + mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); + for (int i=0; i<3; ++i) + mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + } + else + mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); + + if (gameHour >= mTimeSettings.mDayEnd) + { + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); + fade = fade*fade; + mResult.mSunDiscColor.a() = 1.f - fade; + } + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) + { + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; + } + else + mResult.mSunDiscColor.a() = 1; + + mResult.mStormDirection = calculateStormDirection(mResult.mParticleEffect); + } + + inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) + { + calculateResult(mCurrentWeather, gameHour); + const MWRender::WeatherResult current = mResult; + calculateResult(mNextWeather, gameHour); + const MWRender::WeatherResult other = mResult; + + mResult.mStormDirection = current.mStormDirection; + mResult.mNextStormDirection = other.mStormDirection; + + mResult.mCloudTexture = current.mCloudTexture; + mResult.mNextCloudTexture = other.mCloudTexture; + mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); + + mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); + mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); + mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); + + mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); + mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); + mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); + mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); + mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + + mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); + mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); + mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); + + mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); + mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); + mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); + mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + + mResult.mNight = current.mNight; + + float threshold = mWeatherSettings[mNextWeather].mRainThreshold; + if (threshold <= 0.f) + threshold = 0.5f; + + if(factor < threshold) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = 1.f - factor / threshold; + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainDiameter = current.mRainDiameter; + mResult.mRainMinHeight = current.mRainMinHeight; + mResult.mRainMaxHeight = current.mRainMaxHeight; + mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + + mResult.mRainDiameter = other.mRainDiameter; + mResult.mRainMinHeight = other.mRainMinHeight; + mResult.mRainMaxHeight = other.mRainMaxHeight; + mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + } } } + diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index a3928465c4..21b2fae9f8 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -115,6 +115,8 @@ namespace MWWorld class Weather { public: + static osg::Vec3f defaultDirection(); + Weather(const std::string& name, float stormWindSpeed, float rainSpeed, @@ -189,6 +191,8 @@ namespace MWWorld std::string mRainEffect; + osg::Vec3f mStormDirection; + // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature // is broken in the vanilla game and was disabled. diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 6709ee842e..dcd4874e90 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -516,6 +516,10 @@ namespace Shader // We could fall back to a texture size uniform if EXT_gpu_shader4 is missing } + bool simpleLighting = false; + node.getUserValue("simpleLighting", simpleLighting); + defineMap["simpleLighting"] = simpleLighting ? "1" : "0"; + if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT && !previousAddedState->hasMode(GL_ALPHA_TEST)) removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST)); // This disables the deprecated fixed-function alpha test diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 04446d2982..d86719f318 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -36,6 +36,9 @@ set(SHADER_FILES gui_fragment.glsl debug_vertex.glsl debug_fragment.glsl + sky_vertex.glsl + sky_fragment.glsl + skypasses.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index 177017f822..dc9c297a2f 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -87,6 +87,16 @@ void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 a } } +// Simplest lighting which only takes into account sun and ambient. Currently used for our weather particle systems. +void doSimpleLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 ambientLight) +{ + vec3 ambientOut, diffuseOut; + + perLightSun(diffuseOut, viewPos, viewNormal); + ambientLight = gl_LightModel.ambient.xyz; + diffuseLight = diffuseOut; +} + vec3 getSpecular(vec3 viewNormal, vec3 viewDirection, float shininess, vec3 matSpec) { vec3 lightDir = normalize(lcalcPosition(0)); diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index 6f6cede4e4..26107abb10 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -63,7 +63,7 @@ uniform bool simpleWater; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING ((@normalMap || @forcePPL) && !@simpleLighting) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; @@ -170,6 +170,9 @@ void main() #endif float shadowing = unshadowedLightRatio(linearDepth); +#if @simpleLighting + gl_FragData[0].xyz *= passLighting; +#else vec3 lighting; #if !PER_PIXEL_LIGHTING lighting = passLighting + shadowDiffuseLighting * shadowing; @@ -180,8 +183,8 @@ void main() lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; clampLightingResult(lighting); #endif - gl_FragData[0].xyz *= lighting; +#endif #if @envMap && !@preLightEnv gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma; @@ -207,6 +210,7 @@ void main() #endif gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing; } + #if @radialFog float depth; // For the less detailed mesh of simple water we need to recalculate depth on per-pixel basis diff --git a/files/shaders/objects_vertex.glsl b/files/shaders/objects_vertex.glsl index 969fe5903e..5f208ffc25 100644 --- a/files/shaders/objects_vertex.glsl +++ b/files/shaders/objects_vertex.glsl @@ -50,7 +50,7 @@ varying vec2 specularMapUV; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING ((@normalMap || @forcePPL) && !@simpleLighting) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; @@ -126,7 +126,11 @@ void main(void) #if !PER_PIXEL_LIGHTING vec3 diffuseLight, ambientLight; - doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); +#if @simpleLighting + doSimpleLighting(passViewPos, viewNormal, diffuseLight, ambientLight); +#else + doLighting(passViewPos, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); +#endif vec3 emission = getEmissionColor().xyz * emissiveMult; passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; clampLightingResult(passLighting); diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl new file mode 100644 index 0000000000..c064619579 --- /dev/null +++ b/files/shaders/sky_fragment.glsl @@ -0,0 +1,84 @@ +#version 120 + +#include "skypasses.glsl" + +uniform int pass; +uniform sampler2D diffuseMap; +uniform sampler2D maskMap; // PASS_MOON +uniform float opacity; // PASS_CLOUDS, PASS_ATMOSPHERE_NIGHT +uniform vec4 moonBlend; // PASS_MOON +uniform vec4 atmosphereFade; // PASS_MOON + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void paintAtmosphere(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a *= passColor.a; +} + +void paintAtmosphereNight(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; +} + +void paintClouds(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; + color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); +} + +void paintMoon(inout vec4 color) +{ + vec4 phase = texture2D(diffuseMap, diffuseMapUV); + vec4 mask = texture2D(maskMap, diffuseMapUV); + + vec4 blendedLayer = phase * moonBlend; + color = vec4(blendedLayer.xyz + atmosphereFade.xyz, atmosphereFade.a * mask.a); +} + +void paintSun(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= gl_FrontMaterial.diffuse.a; +} + +void paintSunflashQuery(inout vec4 color) +{ + const float threshold = 0.8; + + color = texture2D(diffuseMap, diffuseMapUV); + if (color.a <= threshold) + discard; +} + +void paintSunglare(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a = gl_FrontMaterial.diffuse.a; +} + +void main() +{ + vec4 color = vec4(0.0); + + if (pass == PASS_ATMOSPHERE) + paintAtmosphere(color); + else if (pass == PASS_ATMOSPHERE_NIGHT) + paintAtmosphereNight(color); + else if (pass == PASS_CLOUDS) + paintClouds(color); + else if (pass == PASS_MOON) + paintMoon(color); + else if (pass == PASS_SUN) + paintSun(color); + else if (pass == PASS_SUNFLASH_QUERY) + paintSunflashQuery(color); + else if (pass == PASS_SUNGLARE) + paintSunglare(color); + + gl_FragData[0] = color; +} diff --git a/files/shaders/sky_vertex.glsl b/files/shaders/sky_vertex.glsl new file mode 100644 index 0000000000..9c676140ac --- /dev/null +++ b/files/shaders/sky_vertex.glsl @@ -0,0 +1,20 @@ +#version 120 + +#include "skypasses.glsl" + +uniform mat4 projectionMatrix; +uniform int pass; + +varying vec4 passColor; +varying vec2 diffuseMapUV; + +void main() +{ + gl_Position = projectionMatrix * (gl_ModelViewMatrix * gl_Vertex); + passColor = gl_Color; + + if (pass == PASS_CLOUDS) + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + else + diffuseMapUV = gl_MultiTexCoord0.xy; +} diff --git a/files/shaders/skypasses.glsl b/files/shaders/skypasses.glsl new file mode 100644 index 0000000000..e80d4eb259 --- /dev/null +++ b/files/shaders/skypasses.glsl @@ -0,0 +1,7 @@ +#define PASS_ATMOSPHERE 0 +#define PASS_ATMOSPHERE_NIGHT 1 +#define PASS_CLOUDS 2 +#define PASS_MOON 3 +#define PASS_SUN 4 +#define PASS_SUNFLASH_QUERY 5 +#define PASS_SUNGLARE 6 From 566380c0d663b397b724c993f0b9dc11d23db36d Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 24 Oct 2021 12:37:49 +0400 Subject: [PATCH 013/110] Fix showscenegraph warnings --- components/sceneutil/serialize.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 703b63af7d..134f7c29dd 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -125,6 +125,7 @@ void registerSerializers() "SceneUtil::CompositeStateSetUpdater", "SceneUtil::LightListCallback", "SceneUtil::LightManagerUpdateCallback", + "SceneUtil::NodeCallback", "SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource", @@ -132,6 +133,7 @@ void registerSerializers() "SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder", + "Shader::AddedState", "Shader::RemovedAlphaFunc", "NifOsg::LightManagerStateAttribute", "NifOsg::FlipController", From a58f1a94e3a960e39e4dd5227f7a26cf9b0ee02d Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 23 Oct 2021 00:06:10 +0200 Subject: [PATCH 014/110] Add helpers for binary serialization To construct serializer from given entities: * Data source/destination - any value that has to be serialized/deserialized, usually already existing type. * Format - functional object to define high level serialization logic to define specific format and data schema. Like order of fields, allocation. * Visitor - functional object to define low level serialization logic to operator on given data part. * BinaryWriter - copies given value into provided buffer. * BinaryReader - copies value into given destination from provided buffer. * SizeAccumulator - calculates required buffer size for given data. --- apps/openmw_test_suite/CMakeLists.txt | 4 + .../serialization/binaryreader.cpp | 67 ++++++++++++++++ .../serialization/binarywriter.cpp | 57 +++++++++++++ .../detournavigator/serialization/format.hpp | 75 +++++++++++++++++ .../serialization/integration.cpp | 56 +++++++++++++ .../serialization/sizeaccumulator.cpp | 43 ++++++++++ .../serialization/binaryreader.hpp | 62 ++++++++++++++ .../serialization/binarywriter.hpp | 62 ++++++++++++++ .../detournavigator/serialization/format.hpp | 80 +++++++++++++++++++ .../serialization/sizeaccumulator.hpp | 41 ++++++++++ 10 files changed, 547 insertions(+) create mode 100644 apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp create mode 100644 apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp create mode 100644 apps/openmw_test_suite/detournavigator/serialization/format.hpp create mode 100644 apps/openmw_test_suite/detournavigator/serialization/integration.cpp create mode 100644 apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp create mode 100644 components/detournavigator/serialization/binaryreader.hpp create mode 100644 components/detournavigator/serialization/binarywriter.hpp create mode 100644 components/detournavigator/serialization/format.hpp create mode 100644 components/detournavigator/serialization/sizeaccumulator.hpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 0390b0a772..a2a35e3aab 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -36,6 +36,10 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/serialization/binaryreader.cpp + detournavigator/serialization/binarywriter.cpp + detournavigator/serialization/sizeaccumulator.cpp + detournavigator/serialization/integration.cpp settings/parser.cpp diff --git a/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp new file mode 100644 index 0000000000..d071326cf5 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp @@ -0,0 +1,67 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue) + { + std::uint32_t value = 42; + std::vector data(sizeof(value)); + std::memcpy(data.data(), &value, sizeof(value)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + binaryReader(format, result); + EXPECT_EQ(result, 42); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeRangeValue) + { + const std::size_t count = 3; + std::vector data(sizeof(std::size_t) + count * sizeof(std::uint32_t)); + std::memcpy(data.data(), &count, sizeof(count)); + const std::uint32_t value1 = 960900021; + std::memcpy(data.data() + sizeof(count), &value1, sizeof(std::uint32_t)); + const std::uint32_t value2 = 1235496234; + std::memcpy(data.data() + sizeof(count) + sizeof(std::uint32_t), &value2, sizeof(std::uint32_t)); + const std::uint32_t value3 = 2342038092; + std::memcpy(data.data() + sizeof(count) + 2 * sizeof(std::uint32_t), &value3, sizeof(std::uint32_t)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::size_t resultCount = 0; + const TestFormat format; + binaryReader(format, resultCount); + std::vector result(resultCount); + binaryReader(format, result.data(), result.size()); + EXPECT_THAT(result, ElementsAre(value1, value2, value3)); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeShouldThrowException) + { + std::vector data(3); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + EXPECT_THROW(binaryReader(format, result), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeRangeShouldThrowException) + { + std::vector data(7); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::vector values(2); + const TestFormat format; + EXPECT_THROW(binaryReader(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp new file mode 100644 index 0000000000..fccc2be3da --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp @@ -0,0 +1,57 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue) + { + std::vector result(4); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + binaryWriter(format, std::uint32_t(42)); + EXPECT_THAT(result, ElementsAre(std::byte(42), std::byte(0), std::byte(0), std::byte(0))); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeRangeValue) + { + std::vector result(8); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + binaryWriter(format, values.data(), values.size()); + constexpr std::array expected { + std::byte(42), std::byte(0), std::byte(0), std::byte(0), + std::byte(13), std::byte(0), std::byte(0), std::byte(0), + }; + EXPECT_THAT(result, ElementsAreArray(expected)); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeShouldThrowException) + { + std::vector result(3); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, std::uint32_t(42)), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeRangeShouldThrowException) + { + std::vector result(7); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/format.hpp b/apps/openmw_test_suite/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..7c5e26a0be --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/format.hpp @@ -0,0 +1,75 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include + +#include +#include + +namespace DetourNavigator::SerializationTesting +{ + struct Pod + { + int mInt = 42; + double mDouble = 3.14; + + friend bool operator==(const Pod& l, const Pod& r) + { + const auto tuple = [] (const Pod& v) { return std::tuple(v.mInt, v.mDouble); }; + return tuple(l) == tuple(r); + } + }; + + enum Enum + { + A, + B, + C, + }; + + struct Composite + { + short mFloatArray[3] = {0}; + std::vector mIntVector; + std::vector mEnumVector; + std::vector mPodVector; + std::size_t mPodDataSize = 0; + std::vector mPodBuffer; + std::size_t mCharDataSize = 0; + std::vector mCharBuffer; + }; + + template + struct TestFormat : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Pod>> + { + visitor(*this, value.mInt); + visitor(*this, value.mDouble); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Composite>> + { + visitor(*this, value.mFloatArray); + visitor(*this, value.mIntVector); + visitor(*this, value.mEnumVector); + visitor(*this, value.mPodVector); + visitor(*this, value.mPodDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mPodBuffer.resize(value.mPodDataSize); + visitor(*this, value.mPodBuffer.data(), value.mPodDataSize); + visitor(*this, value.mCharDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mCharBuffer.resize(value.mCharDataSize); + visitor(*this, value.mCharBuffer.data(), value.mCharDataSize); + } + }; +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/serialization/integration.cpp b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp new file mode 100644 index 0000000000..e7e8eacc20 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp @@ -0,0 +1,56 @@ +#include "format.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + struct DetourNavigatorSerializationIntegrationTest : Test + { + Composite mComposite; + + DetourNavigatorSerializationIntegrationTest() + { + mComposite.mIntVector = {4, 5, 6}; + mComposite.mEnumVector = {Enum::A, Enum::B, Enum::C}; + mComposite.mPodVector = {Pod {4, 23.87}, Pod {5, -31.76}, Pod {6, 65.12}}; + mComposite.mPodBuffer = {Pod {7, 456.123}, Pod {8, -628.346}}; + mComposite.mPodDataSize = mComposite.mPodBuffer.size(); + std::string charData = "serialization"; + mComposite.mCharBuffer = {charData.begin(), charData.end()}; + mComposite.mCharDataSize = charData.size(); + } + }; + + TEST_F(DetourNavigatorSerializationIntegrationTest, sizeAccumulatorShouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + TestFormat{}(sizeAccumulator, mComposite); + EXPECT_EQ(sizeAccumulator.value(), 143); + } + + TEST_F(DetourNavigatorSerializationIntegrationTest, binaryReaderShouldDeserializeDataWrittenByBinaryWriter) + { + std::vector data(143); + TestFormat{}(BinaryWriter(data.data(), data.data() + data.size()), mComposite); + Composite result; + TestFormat{}(BinaryReader(data.data(), data.data() + data.size()), result); + EXPECT_EQ(result.mIntVector, mComposite.mIntVector); + EXPECT_EQ(result.mEnumVector, mComposite.mEnumVector); + EXPECT_EQ(result.mPodVector, mComposite.mPodVector); + EXPECT_EQ(result.mPodDataSize, mComposite.mPodDataSize); + EXPECT_EQ(result.mPodBuffer, mComposite.mPodBuffer); + EXPECT_EQ(result.mCharDataSize, mComposite.mCharDataSize); + EXPECT_EQ(result.mCharBuffer, mComposite.mCharBuffer); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp new file mode 100644 index 0000000000..39b7ea8646 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType) + { + SizeAccumulator sizeAccumulator; + constexpr std::monostate format; + sizeAccumulator(format, std::uint32_t()); + EXPECT_EQ(sizeAccumulator.value(), 4); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticTypeRange) + { + SizeAccumulator sizeAccumulator; + const std::uint64_t* const data = nullptr; + const std::size_t count = 3; + const std::monostate format; + sizeAccumulator(format, data, count); + EXPECT_EQ(sizeAccumulator.value(), 24); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + const TestFormat format; + sizeAccumulator(format, Pod {}); + EXPECT_EQ(sizeAccumulator.value(), 12); + } +} diff --git a/components/detournavigator/serialization/binaryreader.hpp b/components/detournavigator/serialization/binaryreader.hpp new file mode 100644 index 0000000000..0d75c3ac99 --- /dev/null +++ b/components/detournavigator/serialization/binaryreader.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + class BinaryReader + { + public: + explicit BinaryReader(const std::byte* pos, const std::byte* end) + : mPos(pos), mEnd(end) + { + assert(mPos <= mEnd); + } + + BinaryReader(const BinaryReader&) = delete; + + template + void operator()(Format&& format, T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(sizeof(value))) + throw std::runtime_error("Not enough data"); + std::memcpy(&value, mPos, sizeof(value)); + mPos += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(count * sizeof(T))) + throw std::runtime_error("Not enough data"); + const std::size_t size = sizeof(T) * count; + std::memcpy(data, mPos, size); + mPos += size; + } + else + { + format(*this, data, count); + } + } + + private: + const std::byte* mPos; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/binarywriter.hpp b/components/detournavigator/serialization/binarywriter.hpp new file mode 100644 index 0000000000..5e710d85d5 --- /dev/null +++ b/components/detournavigator/serialization/binarywriter.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + struct BinaryWriter + { + public: + explicit BinaryWriter(std::byte* dest, const std::byte* end) + : mDest(dest), mEnd(end) + { + assert(mDest <= mEnd); + } + + BinaryWriter(const BinaryWriter&) = delete; + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mDest < static_cast(sizeof(value))) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, &value, sizeof(value)); + mDest += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + const std::size_t size = sizeof(T) * count; + if (mEnd - mDest < static_cast(size)) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, data, size); + mDest += size; + } + else + { + format(*this, data, count); + } + } + + private: + std::byte* mDest; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/format.hpp b/components/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..d07ab9da6f --- /dev/null +++ b/components/detournavigator/serialization/format.hpp @@ -0,0 +1,80 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + enum class Mode + { + Read, + Write, + }; + + template + struct IsContiguousContainer : std::false_type {}; + + template + struct IsContiguousContainer> : std::true_type {}; + + template + constexpr bool isContiguousContainer = IsContiguousContainer>::value; + + template + struct Format + { + template + void operator()(Visitor&& visitor, T* data, std::size_t size) const + { + if constexpr (std::is_arithmetic_v) + { + visitor(self(), data, size); + } + else if constexpr (std::is_enum_v) + { + if constexpr (mode == Mode::Write) + visitor(self(), reinterpret_cast*>(data), size); + else + { + static_assert(mode == Mode::Read); + visitor(self(), reinterpret_cast*>(data), size); + } + } + else + { + std::for_each(data, data + size, [&] (auto& v) { visitor(self(), v); }); + } + } + + template + void operator()(Visitor&& visitor, T(& data)[size]) const + { + self()(std::forward(visitor), data, size); + } + + template + auto operator()(Visitor&& visitor, T&& value) const + -> std::enable_if_t> + { + if constexpr (mode == Mode::Write) + visitor(self(), value.size()); + else + { + static_assert(mode == Mode::Read); + std::size_t size = 0; + visitor(self(), size); + value.resize(size); + } + self()(std::forward(visitor), value.data(), value.size()); + } + + const Derived& self() const { return static_cast(*this); } + }; +} + +#endif diff --git a/components/detournavigator/serialization/sizeaccumulator.hpp b/components/detournavigator/serialization/sizeaccumulator.hpp new file mode 100644 index 0000000000..28bdb5c1cb --- /dev/null +++ b/components/detournavigator/serialization/sizeaccumulator.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H + +#include +#include + +namespace DetourNavigator::Serialization +{ + class SizeAccumulator + { + public: + SizeAccumulator() = default; + + SizeAccumulator(const SizeAccumulator&) = delete; + + std::size_t value() const { return mValue; } + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + mValue += sizeof(T); + else + format(*this, value); + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + mValue += count * sizeof(T); + else + format(*this, data, count); + } + + private: + std::size_t mValue = 0; + }; +} + +#endif From 107a9ecb1701c753804fd9fed04aafa3980f1aaa Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 24 Oct 2021 18:45:04 +0400 Subject: [PATCH 015/110] Fix variables hiding --- apps/openmw/mwclass/creature.cpp | 4 ++-- apps/openmw/mwclass/npc.cpp | 4 ++-- apps/openmw/mwphysics/physicssystem.cpp | 8 ++++---- components/detournavigator/navigatorimpl.cpp | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index a9b1f461ca..4eeab5a393 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -82,7 +82,7 @@ namespace MWClass const Creature::GMST& Creature::getGmst() { - static const GMST gmst = [] + static const GMST staticGmst = [] { GMST gmst; @@ -105,7 +105,7 @@ namespace MWClass return gmst; } (); - return gmst; + return staticGmst; } void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index becfb8719f..ca07d99e3f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -266,7 +266,7 @@ namespace MWClass const Npc::GMST& Npc::getGmst() { - static const GMST gmst = [] + static const GMST staticGmst = [] { GMST gmst; @@ -296,7 +296,7 @@ namespace MWClass return gmst; } (); - return gmst; + return staticGmst; } void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 6c46fd6d05..2a5bcb58bd 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -526,10 +526,10 @@ namespace MWPhysics void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { - if (auto found = mObjects.find(old.mRef); found != mObjects.end()) - found->second->updatePtr(updated); - else if (auto found = mActors.find(old.mRef); found != mActors.end()) - found->second->updatePtr(updated); + if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) + foundObject->second->updatePtr(updated); + else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) + foundActor->second->updatePtr(updated); for (auto& [_, actor] : mActors) { diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index f29ae1bb95..9a59ecf6d7 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -69,8 +69,8 @@ namespace DetourNavigator if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) { const ObjectId avoidId(avoidShape); - const CollisionShape collisionShape {shapes.mShapeInstance, *avoidShape}; - if (mNavMeshManager.updateObject(avoidId, collisionShape, transform, AreaType_null)) + const CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.updateObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; From 0f3c0cb0a0dc637b329cab199932022071c01492 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 24 Oct 2021 18:45:46 +0400 Subject: [PATCH 016/110] Fix argument types mismatch --- .../detournavigator/preparednavmeshdatatuple.hpp | 16 ++++++++-------- components/esm/luascripts.cpp | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp index 8ff1267370..bcca0ace37 100644 --- a/components/detournavigator/preparednavmeshdatatuple.hpp +++ b/components/detournavigator/preparednavmeshdatatuple.hpp @@ -14,11 +14,11 @@ namespace DetourNavigator constexpr auto makeTuple(const rcPolyMesh& v) noexcept { return std::tuple( - Span(v.verts, getVertsLength(v)), - Span(v.polys, getPolysLength(v)), - Span(v.regs, getRegsLength(v)), - Span(v.flags, getFlagsLength(v)), - Span(v.areas, getAreasLength(v)), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.polys, static_cast(getPolysLength(v))), + Span(v.regs, static_cast(getRegsLength(v))), + Span(v.flags, static_cast(getFlagsLength(v))), + Span(v.areas, static_cast(getAreasLength(v))), ArrayRef(v.bmin), ArrayRef(v.bmax), v.cs, @@ -31,9 +31,9 @@ namespace DetourNavigator constexpr auto makeTuple(const rcPolyMeshDetail& v) noexcept { return std::tuple( - Span(v.meshes, getMeshesLength(v)), - Span(v.verts, getVertsLength(v)), - Span(v.tris, getTrisLength(v)) + Span(v.meshes, static_cast(getMeshesLength(v))), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.tris, static_cast(getTrisLength(v))) ); } diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 1dd45ab2b1..7ff841312a 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -32,7 +32,7 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm) { esm.getSubHeader(); data.resize(esm.getSubSize()); - esm.getExact(data.data(), data.size()); + esm.getExact(data.data(), static_cast(data.size())); } return data; } From fef902617aed7271294ab40d377f66f1cc2b224b Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 24 Oct 2021 17:23:15 +0200 Subject: [PATCH 017/110] Parse integer format arguments as variable names --- CHANGELOG.md | 1 + components/compiler/exprparser.cpp | 4 +++- components/compiler/exprparser.hpp | 2 +- components/compiler/lineparser.cpp | 2 +- components/compiler/scanner.cpp | 10 +++++++--- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b37fed31fd..81d3066dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house Bug #6326: Detect Enchantment/Key should detect items in unresolved containers Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures + Bug #6363: Some scripts in Morrowland fail to work Feature #890: OpenMW-CS: Column filtering Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 2d525e6f8c..1aedc8dc59 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -632,7 +632,7 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword) + std::vector& code, int ignoreKeyword, bool expectNames) { bool optional = false; int optionalCount = 0; @@ -717,6 +717,8 @@ namespace Compiler if (optional) parser.setOptional (true); + if(expectNames) + scanner.enableExpectName(); scanner.scan (parser); diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index 2f3eaa8a9f..42739658ec 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,7 +96,7 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword = -1); + std::vector& code, int ignoreKeyword = -1, bool expectNames = false); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 943c052473..ec90812ec7 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -145,7 +145,7 @@ namespace Compiler if (!arguments.empty()) { mExprParser.reset(); - mExprParser.parseArguments (arguments, scanner, mCode); + mExprParser.parseArguments (arguments, scanner, mCode, -1, true); } mName = name; diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 1054e2e269..0e2b76cb23 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -130,7 +130,8 @@ namespace Compiler { bool cont = false; - if (scanInt (c, parser, cont)) + bool scanned = mExpectName ? scanName(c, parser, cont) : scanInt(c, parser, cont); + if (scanned) { mLoc.mLiteral.clear(); return cont; @@ -387,6 +388,8 @@ namespace Compiler bool Scanner::scanSpecial (MultiChar& c, Parser& parser, bool& cont) { + bool expectName = mExpectName; + mExpectName = false; int special = -1; if (c=='\n') @@ -541,15 +544,16 @@ namespace Compiler if (special==S_newline) mLoc.mLiteral = ""; - else if (mExpectName && (special == S_member || special == S_minus)) + else if (expectName && (special == S_member || special == S_minus)) { - mExpectName = false; bool tolerant = mTolerantNames; mTolerantNames = true; bool out = scanName(c, parser, cont); mTolerantNames = tolerant; return out; } + else if (expectName && special == S_comma) + mExpectName = true; TokenLoc loc (mLoc); mLoc.mLiteral.clear(); From 9cbbd2fff56a69ea27091abbd687dc9e967f8b9b Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Sun, 24 Oct 2021 17:13:42 -0700 Subject: [PATCH 018/110] better transitions --- apps/openmw/mwrender/sky.cpp | 3 +++ files/shaders/sky_fragment.glsl | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 217f10f294..9deb6968e3 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -659,7 +659,10 @@ namespace MWRender mParticleNode->addChild(program); for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + { ps->getParticle(particleIndex)->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + ps->getParticle(particleIndex)->update(0, true); + } ps->getOrCreateStateSet(); ps->setUserValue("simpleLighting", true); diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl index c064619579..1ef62ab2cf 100644 --- a/files/shaders/sky_fragment.glsl +++ b/files/shaders/sky_fragment.glsl @@ -29,6 +29,9 @@ void paintClouds(inout vec4 color) color = texture2D(diffuseMap, diffuseMapUV); color.a *= passColor.a * opacity; color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); + + // ease transition between clear color and atmosphere/clouds + color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a * passColor.a); } void paintMoon(inout vec4 color) From 6fb376f1cf6102f5f0d17053f68c2105b16f9348 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Mon, 25 Oct 2021 07:08:50 +0000 Subject: [PATCH 019/110] templatise ptr.hpp (#3185) With this PR we consolidate identical logic for Ptr and ConstPtr with the help of a template. --- apps/openmw/mwworld/livecellref.cpp | 5 + apps/openmw/mwworld/livecellref.hpp | 3 + apps/openmw/mwworld/ptr.cpp | 89 ----------- apps/openmw/mwworld/ptr.hpp | 228 ++++++++-------------------- 4 files changed, 72 insertions(+), 253 deletions(-) delete mode 100644 apps/openmw/mwworld/ptr.cpp diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index c74817911e..62c9f3a2f0 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -73,3 +73,8 @@ bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) { return true; } + +unsigned int MWWorld::LiveCellRefBase::getType() const +{ + return mClass->getType(); +} diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 48e237bce4..1ead0395fd 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -43,6 +43,9 @@ namespace MWWorld virtual std::string_view getTypeDescription() const = 0; + unsigned int getType() const; + ///< @see MWWorld::Class::getType + protected: void loadImp (const ESM::ObjectState& state); diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp deleted file mode 100644 index b18e3b1689..0000000000 --- a/apps/openmw/mwworld/ptr.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "ptr.hpp" - -#include - -#include "containerstore.hpp" -#include "class.hpp" -#include "livecellref.hpp" - -unsigned int MWWorld::Ptr::getType() const -{ - if(mRef != nullptr) - return mRef->mClass->getType(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -MWWorld::CellRef& MWWorld::Ptr::getCellRef() const -{ - assert(mRef); - - return mRef->mRef; -} - -MWWorld::RefData& MWWorld::Ptr::getRefData() const -{ - assert(mRef); - - return mRef->mData; -} - -void MWWorld::Ptr::setContainerStore (ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::Ptr::operator const void *() -{ - return mRef; -} - -// ------------------------------------------------------------------------------- - -unsigned int MWWorld::ConstPtr::getType() const -{ - if(mRef != nullptr) - return mRef->mClass->getType(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::ConstPtr::operator const void *() -{ - return mRef; -} diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index a82abaa212..438b87c7af 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -2,7 +2,7 @@ #define GAME_MWWORLD_PTR_H #include - +#include #include #include @@ -15,20 +15,19 @@ namespace MWWorld struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef - - class Ptr + /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr + template class TypeTransform> + class PtrBase { public: - MWWorld::LiveCellRefBase *mRef; - CellStore *mCell; - ContainerStore *mContainerStore; + typedef TypeTransform LiveCellRefBaseType; + typedef TypeTransform CellStoreType; + typedef TypeTransform ContainerStoreType; - public: - Ptr(MWWorld::LiveCellRefBase *liveCellRef=nullptr, CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } + LiveCellRefBaseType *mRef; + CellStoreType *mCell; + ContainerStoreType *mContainerStore; bool isEmpty() const { @@ -40,7 +39,12 @@ namespace MWWorld // Note 1: ids are not sequential. E.g. for a creature `getType` returns 0x41455243. // Note 2: Life is not easy and full of surprises. For example // prison marker reuses ESM::Door record. Player is ESM::NPC. - unsigned int getType() const; + unsigned int getType() const + { + if(mRef != nullptr) + return mRef->getType(); + throw std::runtime_error("Can't get type name from an empty object."); + } std::string_view getTypeDescription() const { @@ -55,9 +59,9 @@ namespace MWWorld } template - MWWorld::LiveCellRef *get() const + TypeTransform> *get() const { - MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); + TypeTransform> *ref = dynamic_cast>*>(mRef); if(ref) return ref; std::stringstream str; @@ -68,13 +72,26 @@ namespace MWWorld throw std::runtime_error(str.str()); } - MWWorld::LiveCellRefBase *getBase() const; + LiveCellRefBaseType *getBase() const + { + if (!mRef) + throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); + return mRef; + } - MWWorld::CellRef& getCellRef() const; + TypeTransform& getCellRef() const + { + assert(mRef); + return mRef->mRef; + } - RefData& getRefData() const; + TypeTransform& getRefData() const + { + assert(mRef); + return mRef->mData; + } - CellStore *getCell() const + CellStoreType *getCell() const { assert(mCell); return mCell; @@ -85,164 +102,47 @@ namespace MWWorld return (mContainerStore == nullptr) && (mCell != nullptr); } - void setContainerStore (ContainerStore *store); + void setContainerStore (ContainerStoreType *store) ///< Must not be called on references that are in a cell. + { + assert (store); + assert (!mCell); + mContainerStore = store; + } - ContainerStore *getContainerStore() const; + ContainerStoreType *getContainerStore() const ///< May return a 0-pointer, if reference is not in a container. + { + return mContainerStore; + } - operator const void *(); + operator const void *() const ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + { + return mRef; + } + + protected: + PtrBase(LiveCellRefBaseType *liveCellRef, CellStoreType *cell, ContainerStoreType* containerStore) : mRef(liveCellRef), mCell(cell), mContainerStore(containerStore) {} }; - /// \brief Pointer to a const LiveCellRef + /// @note It is possible to get mutable values from const Ptr. So if a function accepts const Ptr&, the object is still mutable. + /// To make it really const the argument should be const ConstPtr&. + class Ptr : public PtrBase + { + public: + Ptr(LiveCellRefBase *liveCellRef=nullptr, CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} + }; + + /// @note The difference between Ptr and ConstPtr is that the second one adds const to the underlying pointers. /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. - class ConstPtr + class ConstPtr : public PtrBase { public: - - const MWWorld::LiveCellRefBase *mRef; - const CellStore *mCell; - const ContainerStore *mContainerStore; - - public: - ConstPtr(const MWWorld::LiveCellRefBase *liveCellRef=nullptr, const CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } - - ConstPtr(const MWWorld::Ptr& ptr) - : mRef(ptr.mRef), mCell(ptr.mCell), mContainerStore(ptr.mContainerStore) - { - } - - bool isEmpty() const - { - return mRef == nullptr; - } - - unsigned int getType() const; - - std::string_view getTypeDescription() const - { - return mRef ? mRef->getTypeDescription() : "nullptr"; - } - - const Class& getClass() const - { - if(mRef != nullptr) - return *(mRef->mClass); - throw std::runtime_error("Cannot get class of an empty object"); - } - - template - const MWWorld::LiveCellRef *get() const - { - const MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); - if(ref) return ref; - - std::stringstream str; - str<< "Bad LiveCellRef cast to "<mRef; - } - - const RefData& getRefData() const - { - assert(mRef); - return mRef->mData; - } - - const CellStore *getCell() const - { - assert(mCell); - return mCell; - } - - bool isInCell() const - { - return (mContainerStore == nullptr) && (mCell != nullptr); - } - - void setContainerStore (const ContainerStore *store); - ///< Must not be called on references that are in a cell. - - const ContainerStore *getContainerStore() const; - ///< May return a 0-pointer, if reference is not in a container. - - operator const void *(); - ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + ConstPtr(const Ptr& ptr) : PtrBase(ptr.mRef, ptr.mCell, ptr.mContainerStore) {} + ConstPtr(const LiveCellRefBase *liveCellRef=nullptr, const CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} }; - inline bool operator== (const Ptr& left, const Ptr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const Ptr& left, const Ptr& right) - { - return !(left==right); - } - - inline bool operator< (const Ptr& left, const Ptr& right) - { - return left.mRef= (const Ptr& left, const Ptr& right) - { - return !(left (const Ptr& left, const Ptr& right) - { - return rightright); - } - - inline bool operator== (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const ConstPtr& left, const ConstPtr& right) - { - return !(left==right); - } - - inline bool operator< (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef= (const ConstPtr& left, const ConstPtr& right) - { - return !(left (const ConstPtr& left, const ConstPtr& right) - { - return rightright); - } } #endif From 7f9beac3a75e8e6d8b3f657591db7f4ecdcbe450 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Mon, 25 Oct 2021 07:18:26 +0000 Subject: [PATCH 020/110] refactors a case insensitive map (#3184) This PR aims to spark the retirement of a questionable pattern I have found all over our code base. I will illustrate how this pattern encourages code duplication, lacks type safety, requires documentation and can be prone to bugs. ``` std::map mMap; // Stored in all lowercase for a case-insensitive lookup std::string lowerKey = Misc::StringUtils::lowerCase(key); mMap.emplace(lowerKey, object); std::string lowerKey = Misc::StringUtils::lowerCase(key); mMap.find(lowerKey); mMap.find(key); // Not found. Oops! ``` An alternative approach produces no such issues. ``` std::unordered_map mMap; mMap.emplace(key, object); mMap.find(key); ``` Of course, such an alternative will work for a `map` as well, but an `unordered_map` is generally preferable over a `map` with these changes because we have moved `lowerCase` into the comparison operator. In this PR I have refactored `Animation::mNodeMap` accordingly. I have reviewed and adapted all direct and indirect usage of `Animation::mNodeMap` to ensure we do not change behaviour with this PR. --- apps/openmw/mwrender/actoranimation.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 5 ++--- apps/openmw/mwrender/animation.hpp | 6 ++++-- apps/openmw/mwrender/creatureanimation.cpp | 4 ++-- apps/openmw/mwrender/npcanimation.cpp | 4 ++-- apps/openmw/mwrender/weaponanimation.cpp | 5 ++--- apps/openmw/mwrender/weaponanimation.hpp | 3 +-- components/misc/stringops.hpp | 16 ++++++++++++++++ components/sceneutil/visitor.cpp | 13 ++++++------- components/sceneutil/visitor.hpp | 6 +++++- 10 files changed, 41 insertions(+), 23 deletions(-) diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index b346d4ac6c..24ca0aa4f3 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -74,7 +74,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return PartHolderPtr(); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index ca76856d48..cdc6c49ab2 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1510,7 +1510,7 @@ namespace MWRender parentNode = mInsert; else { - NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + bonename); @@ -1619,8 +1619,7 @@ namespace MWRender const osg::Node* Animation::getNode(const std::string &name) const { - std::string lowerName = Misc::StringUtils::lowerCase(name); - NodeMap::const_iterator found = getNodeMap().find(lowerName); + NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 84788c1e2d..d37d548dd3 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -7,8 +7,10 @@ #include #include #include +#include #include +#include namespace ESM { @@ -157,6 +159,8 @@ public: virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -250,8 +254,6 @@ protected: std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; - // Stored in all lowercase for a case-insensitive lookup - typedef std::map > NodeMap; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index b64b205961..657179db75 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -126,7 +126,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) bonename = "Weapon Bone"; } @@ -161,7 +161,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(itemModel); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get(), mResourceSystem->getSceneManager()); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 42215c00bc..f3d026ad3c 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -713,7 +713,7 @@ PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const st osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); @@ -813,7 +813,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g if (weaponBonename != bonename) { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); + NodeMap::const_iterator found = nodeMap.find(weaponBonename); if (found != nodeMap.end()) bonename = weaponBonename; } diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 3db415126b..0572eae3be 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -172,14 +172,13 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) } } -void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) +void WeaponAnimation::addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { mSpineControllers[i] = nullptr; - std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); + Animation::NodeMap::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); if (found != nodes.end()) { osg::Node* node = found->second; diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index 1f614463a6..125587c1bd 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -42,8 +42,7 @@ namespace MWRender void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. - void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + void addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 0863522356..6c9c1eefa2 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "utf8stream.hpp" @@ -182,6 +183,21 @@ public: return out; } + struct CiEqual + { + bool operator()(const std::string& left, const std::string& right) const + { + return ciEqual(left, right); + } + }; + struct CiHash + { + std::size_t operator()(std::string str) const + { + lowerCaseInPlace(str); + return std::hash{}(str); + } + }; struct CiComp { bool operator()(const std::string& left, const std::string& right) const diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index fde87c66ba..ea09445678 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -51,18 +51,17 @@ namespace SceneUtil void NodeMapVisitor::apply(osg::MatrixTransform& trans) { - // Take transformation for first found node in file - std::string originalNodeName = Misc::StringUtils::lowerCase(trans.getName()); + // Choose first found node in file if (trans.libraryName() == std::string("osgAnimation")) { + std::string nodeName = trans.getName(); // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses whitespace-separated names) - std::replace(originalNodeName.begin(), originalNodeName.end(), '_', ' '); + std::replace(nodeName.begin(), nodeName.end(), '_', ' '); + mMap.emplace(nodeName, &trans); } - - const std::string nodeName = originalNodeName; - - mMap.emplace(nodeName, &trans); + else + mMap.emplace(trans.getName(), &trans); traverse(trans); } diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index fcf3c1f944..45aa408b9e 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -4,6 +4,10 @@ #include #include +#include + +#include + // Commonly used scene graph visitors namespace SceneUtil { @@ -49,7 +53,7 @@ namespace SceneUtil class NodeMapVisitor : public osg::NodeVisitor { public: - typedef std::map > NodeMap; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; NodeMapVisitor(NodeMap& map) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) From 1ff8318a526fcfd68a5fe2e5670e01fb4309403f Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Mon, 25 Oct 2021 07:28:32 +0000 Subject: [PATCH 021/110] refactors premultiplied alpha (#3189) With this PR we refactor a `premultiplied alpha` user string set by `characterpreview.cpp` into a more flexible mechanism allowing us to assign any state to GUI textures. We can consider these changes more future proof than the previous approach. --- apps/openmw/mwgui/inventorywindow.cpp | 2 +- apps/openmw/mwgui/race.cpp | 2 +- apps/openmw/mwrender/characterpreview.cpp | 4 +++- apps/openmw/mwrender/characterpreview.hpp | 4 ++++ .../myguiplatform/myguirendermanager.cpp | 20 +++++-------------- components/myguiplatform/myguitexture.cpp | 4 +++- components/myguiplatform/myguitexture.hpp | 6 +++++- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index a586991888..0e76c2429f 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -70,7 +70,7 @@ namespace MWGui , mTrading(false) , mUpdateTimer(0.f) { - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 457594697d..d30eb65eb3 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -138,7 +138,7 @@ namespace MWGui mPreview->rebuild(); mPreview->setAngle (mCurrentAngle); - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 4a84bf4f23..c717806e35 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -190,7 +190,9 @@ namespace MWRender mTexture->setInternalFormat(GL_RGBA); mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mTexture->setUserValue("premultiplied alpha", true); + + mTextureStateSet = new osg::StateSet; + mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); mCamera = new osg::Camera; // hints that the camera is not relative to the master camera diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 3eb9688465..808ff0801d 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -18,6 +18,7 @@ namespace osg class Camera; class Group; class Viewport; + class StateSet; } namespace MWRender @@ -41,6 +42,8 @@ namespace MWRender void rebuild(); osg::ref_ptr getTexture(); + /// Get the osg::StateSet required to render the texture correctly, if any. + osg::StateSet* getTextureStateSet() { return mTextureStateSet; } private: CharacterPreview(const CharacterPreview&); @@ -54,6 +57,7 @@ namespace MWRender osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mTexture; + osg::ref_ptr mTextureStateSet; osg::ref_ptr mCamera; osg::ref_ptr mDrawOnceCallback; diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 3061b329c2..43b176c795 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -4,10 +4,8 @@ #include #include -#include #include #include -#include #include @@ -17,8 +15,6 @@ #include #include -#include - #include "myguicompat.h" #include "myguitexture.hpp" @@ -465,23 +461,17 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text batch.mVertexBuffer = static_cast(buffer)->getVertexBuffer(); batch.mArray = static_cast(buffer)->getVertexArray(); static_cast(buffer)->markUsed(); - bool premultipliedAlpha = false; - if (texture) + + if (OSGTexture* osgtexture = static_cast(texture)) { - batch.mTexture = static_cast(texture)->getTexture(); + batch.mTexture = osgtexture->getTexture(); if (batch.mTexture->getDataVariance() == osg::Object::DYNAMIC) mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin() - batch.mTexture->getUserValue("premultiplied alpha", premultipliedAlpha); + if (!mInjectState && osgtexture->getInjectState()) + batch.mStateSet = osgtexture->getInjectState(); } if (mInjectState) batch.mStateSet = mInjectState; - else if (premultipliedAlpha) - { - // This is hacky, but MyGUI made it impossible to use a custom layer for a nested node, so state couldn't be injected 'properly' - osg::ref_ptr stateSet = new osg::StateSet(); - stateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); - batch.mStateSet = stateSet; - } mDrawable->addBatch(batch); } diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index ce7332cc7e..d0e4e9a86e 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -21,9 +22,10 @@ namespace osgMyGUI { } - OSGTexture::OSGTexture(osg::Texture2D *texture) + OSGTexture::OSGTexture(osg::Texture2D *texture, osg::StateSet *injectState) : mImageManager(nullptr) , mTexture(texture) + , mInjectState(injectState) , mFormat(MyGUI::PixelFormat::Unknow) , mUsage(MyGUI::TextureUsage::Default) , mNumElemBytes(0) diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp index a34f1b7628..e8b49eab04 100644 --- a/components/myguiplatform/myguitexture.hpp +++ b/components/myguiplatform/myguitexture.hpp @@ -15,6 +15,7 @@ namespace osg { class Image; class Texture2D; + class StateSet; } namespace Resource @@ -31,6 +32,7 @@ namespace osgMyGUI osg::ref_ptr mLockedImage; osg::ref_ptr mTexture; + osg::ref_ptr mInjectState; MyGUI::PixelFormat mFormat; MyGUI::TextureUsage mUsage; size_t mNumElemBytes; @@ -40,9 +42,11 @@ namespace osgMyGUI public: OSGTexture(const std::string &name, Resource::ImageManager* imageManager); - OSGTexture(osg::Texture2D* texture); + OSGTexture(osg::Texture2D* texture, osg::StateSet* injectState = nullptr); virtual ~OSGTexture(); + osg::StateSet* getInjectState() { return mInjectState; } + const std::string& getName() const override { return mName; } void createManual(int width, int height, MyGUI::TextureUsage usage, MyGUI::PixelFormat format) override; From 13b84ce798258fd4b1349c13904fe3feac292a46 Mon Sep 17 00:00:00 2001 From: psi29a Date: Mon, 25 Oct 2021 10:42:14 +0000 Subject: [PATCH 022/110] Fix MacOS failure for 10.15 by setting it to `allow_failure: true` --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9b8cf9341..b69e930ae2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -184,7 +184,6 @@ Debian_Clang_tests_Debug: macOS11_Xcode12: extends: .MacOS image: macos-11-xcode-12 - allow_failure: true cache: key: macOS11_Xcode12.v1 variables: @@ -193,6 +192,7 @@ macOS11_Xcode12: macOS10.15_Xcode11: extends: .MacOS image: macos-10.15-xcode-11 + allow_failure: true cache: key: macOS10.15_Xcode11.v1 variables: From 289e1544d21f73ea9bf74c390ee46e35388d0814 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 25 Oct 2021 16:54:50 +0200 Subject: [PATCH 023/110] Don't wait a frame to recalculate magicka --- CHANGELOG.md | 1 + apps/openmw/mwclass/creature.cpp | 10 ++--- apps/openmw/mwclass/npc.cpp | 14 +++--- apps/openmw/mwmechanics/actors.cpp | 45 ------------------- apps/openmw/mwmechanics/actors.hpp | 4 -- apps/openmw/mwmechanics/actorutil.cpp | 8 ++++ apps/openmw/mwmechanics/actorutil.hpp | 8 ++++ apps/openmw/mwmechanics/creaturestats.cpp | 45 +++++++++++-------- apps/openmw/mwmechanics/creaturestats.hpp | 5 +-- .../mwmechanics/mechanicsmanagerimp.cpp | 3 +- apps/openmw/mwmechanics/spelleffects.cpp | 13 +++--- 11 files changed, 67 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b37fed31fd..58d111dd01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Bug #3246: ESSImporter: Most NPCs are dead on save load Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) + Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes Bug #3905: Great House Dagoth issues Bug #4203: Resurrecting an actor should close the loot GUI diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index a9b1f461ca..4ca5b37cf8 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -112,7 +112,10 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureCustomData); + auto tempData = std::make_unique(); + CreatureCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -156,10 +159,7 @@ namespace MWClass data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); - data->mCreatureStats.setNeedRecalcDynamicStats(false); - - // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index becfb8719f..b8df6747b3 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -303,7 +303,11 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new NpcCustomData); + bool recalculate = false; + auto tempData = std::make_unique(); + NpcCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -334,8 +338,6 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); - - data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -351,7 +353,7 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); - data->mNpcStats.setNeedRecalcDynamicStats(true); + recalculate = true; } // Persistent actors with 0 health do not play death animation @@ -387,7 +389,9 @@ namespace MWClass data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; + if(recalculate) + data->mNpcStats.recalculateMagicka(); // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index d85946dbd5..0af65844f9 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -240,10 +240,7 @@ namespace MWMechanics { // magic effects adjustMagicEffects (ptr, duration); - if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) - calculateDynamicStats (ptr); - calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } @@ -654,29 +651,6 @@ namespace MWMechanics updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } - void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) - { - CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - - float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - - float base = 1.f; - if (ptr == getPlayer()) - base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); - else - base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); - - double magickaFactor = base + - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; - - DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); - float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; - magicka.setModified(magicka.getModified() + diff, 0); - magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); - creatureStats.setMagicka(magicka); - } - void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); @@ -771,14 +745,6 @@ namespace MWMechanics stats.setFatigue (fatigue); } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - - if (creatureStats.needToRecalcDynamicStats()) - calculateDynamicStats(ptr); - } - bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); @@ -1711,10 +1677,6 @@ namespace MWMechanics if (iter->first.getType() == ESM::Creature::sRecordId) soulTrap(iter->first); - // Magic effects will be reset later, and the magic effect that could kill the actor - // needs to be determined now - calculateCreatureStatModifiers(iter->first, 0); - if (cls.isEssential(iter->first)) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } @@ -1730,8 +1692,6 @@ namespace MWMechanics // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); - // Reset dynamic stats, attributes and skills - calculateCreatureStatModifiers(iter->first, 0); stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) @@ -1816,10 +1776,6 @@ namespace MWMechanics continue; adjustMagicEffects (iter->first, duration); - if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) - calculateDynamicStats (iter->first); - - calculateCreatureStatModifiers (iter->first, duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2209,7 +2165,6 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { adjustMagicEffects(ptr, 0.f); - calculateCreatureStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 9950a591ab..7d44fd06cd 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -43,10 +43,6 @@ namespace MWMechanics void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); - void calculateDynamicStats (const MWWorld::Ptr& ptr); - - void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateRestoration (const MWWorld::Ptr& ptr, float duration); void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index 04cbb8e9f5..4a587dd662 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -29,4 +29,12 @@ namespace MWMechanics const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; } + + CreatureCustomDataResetter::CreatureCustomDataResetter(const MWWorld::Ptr& ptr) : mPtr(ptr) {} + + CreatureCustomDataResetter::~CreatureCustomDataResetter() + { + if(!mPtr.isEmpty()) + mPtr.getRefData().setCustomData({}); + } } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index a226fc9cb6..a66d8866bf 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -86,6 +86,14 @@ namespace MWMechanics template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); + + struct CreatureCustomDataResetter + { + MWWorld::Ptr mPtr; + + CreatureCustomDataResetter(const MWWorld::Ptr& ptr); + ~CreatureCustomDataResetter(); + }; } #endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 8ca6e8b766..6710dcd734 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -7,6 +7,7 @@ #include #include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" @@ -22,7 +23,7 @@ namespace MWMechanics mTalkedTo (false), mAlarmed (false), mAttacked (false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), - mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), + mFallHeight(0), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) { for (int i=0; i<4; ++i) @@ -146,7 +147,7 @@ namespace MWMechanics mAttributes[index] = value; if (index == ESM::Attribute::Intelligence) - mRecalcMagicka = true; + recalculateMagicka(); else if (index == ESM::Attribute::Strength || index == ESM::Attribute::Willpower || index == ESM::Attribute::Agility || @@ -208,11 +209,10 @@ namespace MWMechanics void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcMagicka = true; - + bool recalc = effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier(); mMagicEffects.setModifiers(effects); + if(recalc) + recalculateMagicka(); } void CreatureStats::setAiSetting (AiSetting index, Stat value) @@ -400,19 +400,26 @@ namespace MWMechanics return height; } - bool CreatureStats::needToRecalcDynamicStats() + void CreatureStats::recalculateMagicka() { - if (mRecalcMagicka) - { - mRecalcMagicka = false; - return true; - } - return false; - } + auto world = MWBase::Environment::get().getWorld(); + float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); - void CreatureStats::setNeedRecalcDynamicStats(bool val) - { - mRecalcMagicka = val; + float base = 1.f; + const auto& player = world->getPlayerPtr(); + if (this == &player.getClass().getCreatureStats(player)) + base = world->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); + else + base = world->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + + double magickaFactor = base + mMagicEffects.get(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + + DynamicStat magicka = getMagicka(); + float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); + float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; + magicka.setModified(magicka.getModified() + diff, 0); + magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); + setMagicka(magicka); } void CreatureStats::setKnockedDown(bool value) @@ -532,7 +539,6 @@ namespace MWMechanics state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; - state.mRecalcDynamicStats = mRecalcMagicka; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; @@ -586,7 +592,6 @@ namespace MWMechanics mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; mLastHitAttemptObject = state.mLastHitAttemptObject; - mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; @@ -627,6 +632,8 @@ namespace MWMechanics if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); + if(state.mRecalcDynamicStats) + recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index d234d14486..e5c4f6fa41 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -65,8 +65,6 @@ namespace MWMechanics std::string mLastHitObject; // The last object to hit this actor std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - bool mRecalcMagicka; - // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -103,8 +101,7 @@ namespace MWMechanics DrawState_ getDrawState() const; void setDrawState(DrawState_ state); - bool needToRecalcDynamicStats(); - void setNeedRecalcDynamicStats(bool val); + void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 149056e605..e01ed3c425 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -77,7 +77,7 @@ namespace MWMechanics MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); - npcStats.setNeedRecalcDynamicStats(true); + npcStats.recalculateMagicka(); const ESM::NPC *player = ptr.get()->mBase; @@ -222,7 +222,6 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); - mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 0e67f06ba2..d9e32b8b9f 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -279,7 +279,7 @@ namespace namespace MWMechanics { -void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage, bool& recalculateMagicka) { const auto world = MWBase::Environment::get().getWorld(); bool godmode = target == getPlayer() && world->getGodModeState(); @@ -609,7 +609,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co fortifySkill(target, effect, effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + recalculateMagicka = true; break; case ESM::MagicEffect::AbsorbHealth: case ESM::MagicEffect::AbsorbMagicka: @@ -687,13 +687,14 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac const auto world = MWBase::Environment::get().getWorld(); bool invalid = false; bool receivedMagicDamage = false; + bool recalculateMagicka = false; if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) { spellParams.worsen(); for(auto& otherEffect : spellParams.getEffects()) { if(isCorprusEffect(otherEffect)) - applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, recalculateMagicka); } if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); @@ -815,7 +816,7 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac if(effect.mEffectId == ESM::MagicEffect::Corprus) spellParams.worsen(); else - applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, recalculateMagicka); effect.mMagnitude = magnitude; magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); } @@ -832,6 +833,8 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; if (receivedMagicDamage && target == getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + if(recalculateMagicka) + target.getClass().getCreatureStats(target).recalculateMagicka(); return false; } @@ -981,7 +984,7 @@ void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellPara fortifySkill(target, effect, -effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + target.getClass().getCreatureStats(target).recalculateMagicka(); break; case ESM::MagicEffect::AbsorbAttribute: { From 07e32c0fa6be9fa7129c1002b77deb469803d248 Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Mon, 25 Oct 2021 10:23:16 -0700 Subject: [PATCH 024/110] remove object shader path --- apps/openmw/mwworld/weather.cpp | 1 - components/shader/shadervisitor.cpp | 6 +++++- files/shaders/lighting.glsl | 10 ---------- files/shaders/objects_fragment.glsl | 8 ++------ files/shaders/objects_vertex.glsl | 8 ++------ files/shaders/sky_fragment.glsl | 2 +- 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 9055b95ee1..965c690238 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -1273,4 +1273,3 @@ namespace MWWorld } } - diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index dcd4874e90..9877ab1863 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -518,7 +518,11 @@ namespace Shader bool simpleLighting = false; node.getUserValue("simpleLighting", simpleLighting); - defineMap["simpleLighting"] = simpleLighting ? "1" : "0"; + if (simpleLighting) + { + defineMap["forcePPL"] = "1"; + defineMap["endLight"] = "0"; + } if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT && !previousAddedState->hasMode(GL_ALPHA_TEST)) removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST)); diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index dc9c297a2f..177017f822 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -87,16 +87,6 @@ void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 a } } -// Simplest lighting which only takes into account sun and ambient. Currently used for our weather particle systems. -void doSimpleLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 ambientLight) -{ - vec3 ambientOut, diffuseOut; - - perLightSun(diffuseOut, viewPos, viewNormal); - ambientLight = gl_LightModel.ambient.xyz; - diffuseLight = diffuseOut; -} - vec3 getSpecular(vec3 viewNormal, vec3 viewDirection, float shininess, vec3 matSpec) { vec3 lightDir = normalize(lcalcPosition(0)); diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index 26107abb10..6f6cede4e4 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -63,7 +63,7 @@ uniform bool simpleWater; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING ((@normalMap || @forcePPL) && !@simpleLighting) +#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; @@ -170,9 +170,6 @@ void main() #endif float shadowing = unshadowedLightRatio(linearDepth); -#if @simpleLighting - gl_FragData[0].xyz *= passLighting; -#else vec3 lighting; #if !PER_PIXEL_LIGHTING lighting = passLighting + shadowDiffuseLighting * shadowing; @@ -183,8 +180,8 @@ void main() lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; clampLightingResult(lighting); #endif + gl_FragData[0].xyz *= lighting; -#endif #if @envMap && !@preLightEnv gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma; @@ -210,7 +207,6 @@ void main() #endif gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing; } - #if @radialFog float depth; // For the less detailed mesh of simple water we need to recalculate depth on per-pixel basis diff --git a/files/shaders/objects_vertex.glsl b/files/shaders/objects_vertex.glsl index 5f208ffc25..969fe5903e 100644 --- a/files/shaders/objects_vertex.glsl +++ b/files/shaders/objects_vertex.glsl @@ -50,7 +50,7 @@ varying vec2 specularMapUV; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING ((@normalMap || @forcePPL) && !@simpleLighting) +#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; @@ -126,11 +126,7 @@ void main(void) #if !PER_PIXEL_LIGHTING vec3 diffuseLight, ambientLight; -#if @simpleLighting - doSimpleLighting(passViewPos, viewNormal, diffuseLight, ambientLight); -#else - doLighting(passViewPos, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); -#endif + doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); vec3 emission = getEmissionColor().xyz * emissiveMult; passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; clampLightingResult(passLighting); diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl index 1ef62ab2cf..cfa3650c02 100644 --- a/files/shaders/sky_fragment.glsl +++ b/files/shaders/sky_fragment.glsl @@ -31,7 +31,7 @@ void paintClouds(inout vec4 color) color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); // ease transition between clear color and atmosphere/clouds - color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a * passColor.a); + color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a); } void paintMoon(inout vec4 color) From ba4f1921abf22c0b1af85b52c1eebbfb8c780d47 Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Mon, 25 Oct 2021 15:04:55 -0700 Subject: [PATCH 025/110] overrides --- apps/openmw/mwrender/sky.cpp | 6 ++-- apps/openmw/mwrender/skyutil.cpp | 58 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 9deb6968e3..eb7c4a3882 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -340,7 +340,7 @@ namespace MWRender auto depth = SceneUtil::createDepth(); depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); @@ -374,7 +374,7 @@ namespace MWRender raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, raindropTex); stateset->setNestRenderBins(false); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); @@ -384,7 +384,7 @@ namespace MWRender mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + stateset->setAttributeAndModes(mat); osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp index ae689547fd..9932733fa1 100644 --- a/apps/openmw/mwrender/skyutil.cpp +++ b/apps/openmw/mwrender/skyutil.cpp @@ -126,7 +126,7 @@ namespace MWRender void setDefaults(osg::StateSet* stateset) override { - stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON); + stateset->setAttributeAndModes(MWRender::createUnlitMaterial()); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override @@ -292,7 +292,7 @@ namespace MWRender mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + stateset->setAttributeAndModes(mat); cv->pushStateSet(stateset); traverse(node, cv); @@ -359,9 +359,9 @@ namespace MWRender { if (mForceShaders) { - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon))); + stateset->setTextureAttributeAndModes(0, mPhaseTex); + stateset->setTextureAttributeAndModes(1, mCircleTex); stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); @@ -372,15 +372,15 @@ namespace MWRender } else { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, mPhaseTex); osg::ref_ptr texEnv = new osg::TexEnvCombine; texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, texEnv); - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(1, mCircleTex); osg::ref_ptr texEnv2 = new osg::TexEnvCombine; texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); @@ -389,7 +389,7 @@ namespace MWRender texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(1, texEnv2); stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } } @@ -477,7 +477,7 @@ namespace MWRender void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) { stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere))); } void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) @@ -501,8 +501,8 @@ namespace MWRender { if (mForceShaders) { - stateset->addUniform(new osg::Uniform("opacity", 0.f), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("opacity", 0.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night))); } else { @@ -561,18 +561,18 @@ namespace MWRender stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); osg::ref_ptr texmat = new osg::TexMat; - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, texmat); if (mForceShaders) { stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->addUniform(new osg::Uniform("opacity", 1.f), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("opacity", 1.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds))); } else { - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(1, texmat); // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); @@ -582,7 +582,7 @@ namespace MWRender texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(1, texEnvCombine); stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); @@ -715,8 +715,8 @@ namespace MWRender sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); sunTex->setName("diffuseMap"); - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex); + mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); osg::ref_ptr queryNode = new osg::Group; // Need to render after the world geometry so we can correctly test for occlusions @@ -726,14 +726,14 @@ namespace MWRender // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun osg::ref_ptr alphaFunc = new osg::AlphaFunc; alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(alphaFunc); + stateset->setTextureAttributeAndModes(0, sunTex); + stateset->setAttributeAndModes(createUnlitMaterial()); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query))); // Disable writing to the color buffer. We are using this geometry for visibility tests only. osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); - stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); + stateset->setAttributeAndModes(colormask); mTransform->addChild(queryNode); @@ -829,7 +829,7 @@ namespace MWRender depth->setZNear(far); depth->setZFar(far); depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); + queryStateSet->setAttributeAndModes(depth); } else { @@ -859,11 +859,11 @@ namespace MWRender osg::StateSet* stateset = geom->getOrCreateStateSet(); - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, tex); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); mSunFlashNode = group; @@ -898,13 +898,13 @@ namespace MWRender stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare)), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare))); // set up additive blending osg::ref_ptr blendFunc = new osg::BlendFunc; blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + stateset->setAttributeAndModes(blendFunc); mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); mSunGlareNode = camera; From 4ec927829f41a2b204feaf477bdc2cc402bbe47d Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 10 Oct 2021 16:28:45 +0200 Subject: [PATCH 026/110] Give each reflect and spell absorption effect a chance to apply --- CHANGELOG.md | 2 + apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwmechanics/activespells.cpp | 30 ++++- apps/openmw/mwmechanics/activespells.hpp | 2 + apps/openmw/mwmechanics/spellabsorption.cpp | 82 ------------ apps/openmw/mwmechanics/spellabsorption.hpp | 17 --- apps/openmw/mwmechanics/spellcasting.cpp | 63 +--------- apps/openmw/mwmechanics/spellcasting.hpp | 2 +- apps/openmw/mwmechanics/spelleffects.cpp | 131 ++++++++++++++++---- apps/openmw/mwmechanics/spelleffects.hpp | 7 +- apps/openmw/mwworld/esmloader.cpp | 8 +- apps/openmw/mwworld/projectilemanager.cpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 2 +- components/esm/activespells.hpp | 4 +- 14 files changed, 162 insertions(+), 192 deletions(-) delete mode 100644 apps/openmw/mwmechanics/spellabsorption.cpp delete mode 100644 apps/openmw/mwmechanics/spellabsorption.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d609b0ae7f..d2968ae9d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ Bug #6174: Spellmaking and Enchanting sliders differences from vanilla Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla Bug #6197: Infinite Casting Loop + Bug #6253: Multiple instances of Reflect stack additively + Bug #6255: Reflect is different from vanilla Bug #6258: Barter menu glitches out when modifying prices Bug #6273: Respawning NPCs rotation is inconsistent Bug #6282: Laura craft doesn't follow the player character diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 7334d893db..89620d6dc1 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -94,7 +94,7 @@ add_openmw_dir (mwmechanics aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning character actors objects aistate trading weaponpriority spellpriority weapontype spellutil - spellabsorption spelleffects + spelleffects ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index fc35e40b74..d435bd6c44 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,5 +1,7 @@ #include "activespells.hpp" +#include + #include #include @@ -14,6 +16,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -96,6 +100,11 @@ namespace MWMechanics , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) {} + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) + : mId(params.mId), mDisplayName(params.mDisplayName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(params.mSlot), mType(params.mType), mWorsenings(-1) + {} + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { ESM::ActiveSpells::ActiveSpellParams params; @@ -220,10 +229,19 @@ namespace MWMechanics { const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? bool removedSpell = false; + std::optional reflected; for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); - if(remove) + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(result == MagicApplicationResult::REFLECTED) + { + if(!reflected) + reflected = {*spellIt, ptr}; + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if(result == MagicApplicationResult::REMOVED) it = spellIt->mEffects.erase(it); else ++it; @@ -231,6 +249,14 @@ namespace MWMechanics if(removedSpell) break; } + if(reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_Reflect"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if(animation && !reflectStatic->mModel.empty()) + animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } if(removedSpell) continue; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 0538d7a8b7..524bdc0475 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -50,6 +50,8 @@ namespace MWMechanics ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor); + ESM::ActiveSpells::ActiveSpellParams toEsm() const; friend class ActiveSpells; diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp deleted file mode 100644 index 82531132ca..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "spellabsorption.hpp" - -#include - -#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" -#include "spellutil.hpp" - -namespace MWMechanics -{ - float getProbability(const MWMechanics::ActiveSpells& activeSpells) - { - float probability = 0.f; - for(const auto& params : activeSpells) - { - for(const auto& effect : params.getEffects()) - { - if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) - { - if(probability == 0.f) - probability = effect.mMagnitude / 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 - probability; - failProbability *= 1.f - effect.mMagnitude / 100; - probability = 1.f - failProbability; - } - } - } - } - return static_cast(probability * 100); - } - - bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) - { - if (spellId.empty() || target.isEmpty() || 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; - - int chance = getProbability(stats.getActiveSpells()); - if (Misc::Rng::roll0to99() >= chance) - return false; - - const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Static* absorbStatic = esmStore.get().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()); - const ESM::Spell* spell = esmStore.get().search(spellId); - int spellCost = 0; - if (spell) - { - spellCost = MWMechanics::calcSpellCost(*spell); - } - else - { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); - if (enchantment) - spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); - } - - // Magicka is increased by the cost of the spell - DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spellCost); - stats.setMagicka(magicka); - return true; - } - -} diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp deleted file mode 100644 index 0fe501df91..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MWMECHANICS_SPELLABSORPTION_H -#define MWMECHANICS_SPELLABSORPTION_H - -#include - -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 std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); -} - -#endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 026f6b5f19..2edc437752 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,38 +22,11 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "spellabsorption.hpp" #include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" #include "weapontype.hpp" -namespace -{ - 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().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; - } -} - namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) @@ -82,7 +55,7 @@ namespace MWMechanics } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, - const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) + const ESM::EffectList &effects, ESM::RangeType range, bool exploded) { const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) @@ -123,7 +96,6 @@ namespace MWMechanics } } - ESM::EffectList reflectedEffects; ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); @@ -136,9 +108,6 @@ namespace MWMechanics // throughout the iteration of this spell's // effects, we display a "can't re-cast" message - // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(mId, caster, target); - int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) @@ -167,19 +136,6 @@ namespace MWMechanics && (caster.isEmpty() || !caster.getClass().isActor())) continue; - // Notify the target actor they've been hit - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); - - // Avoid proceeding further for absorbed spells. - if (absorbed) - continue; - - // Reflect harmful effects - if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) - continue; - ActiveSpells::ActiveEffect effect; effect.mEffectId = effectIt->mEffectID; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; @@ -189,13 +145,8 @@ namespace MWMechanics effect.mTimeLeft = 0.f; effect.mEffectIndex = currentEffectIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMinMagnitude = 0; - effect.mMaxMagnitude = 0; - } + if(mManualSpell) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; @@ -209,14 +160,14 @@ namespace MWMechanics // add to list of active effects, to apply in next frame params.getEffects().emplace_back(effect); - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. MWBase::Environment::get().getWindowManager()->setEnemy(target); } - if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { playEffects(target, *magicEffect); } @@ -227,9 +178,6 @@ namespace MWMechanics if (!target.isEmpty()) { - if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true, exploded); - if (!params.getEffects().empty()) { if(targetIsActor) @@ -237,6 +185,7 @@ namespace MWMechanics else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + // and we can ignore reflection since non-actors cannot reflect spells for(auto& effect : params.getEffects()) applyMagicEffect(target, caster, params, effect, 0.f); } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index a21ea33e0b..4d9cc3a7de 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -62,7 +62,7 @@ namespace MWMechanics /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); + const ESM::EffectList& effects, ESM::RangeType range, bool exploded=false); }; void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index d9e32b8b9f..7907642f5e 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -16,6 +16,7 @@ #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwrender/animation.hpp" @@ -261,6 +262,97 @@ namespace return false; } + void absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + { + const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Static* absorbStatic = esmStore.get().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()); + const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; + if (spell) + { + spellCost = MWMechanics::calcSpellCost(*spell); + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + if (enchantment) + spellCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); + } + + // Magicka is increased by the cost of the spell + auto& stats = target.getClass().getCreatureStats(target); + auto magicka = stats.getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spellCost); + stats.setMagicka(magicka); + } + + MWMechanics::MagicApplicationResult applyProtections(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, const ESM::MagicEffect* magicEffect) + { + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + // Apply reflect and spell absorption + if(target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment && spellParams.getType() != ESM::ActiveSpells::Type_Permanent) + { + bool canReflect = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && + !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) && magnitudes.get(ESM::MagicEffect::Reflect).getMagnitude() > 0.f; + bool canAbsorb = !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_SpellAbsorption) && magnitudes.get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f; + if(canReflect || canAbsorb) + { + for(const auto& activeParam : stats.getActiveSpells()) + { + for(const auto& activeEffect : activeParam.getEffects()) + { + if(!(activeEffect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + if(activeEffect.mEffectId == ESM::MagicEffect::Reflect) + { + if(canReflect && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + return MWMechanics::MagicApplicationResult::REFLECTED; + } + } + else if(activeEffect.mEffectId == ESM::MagicEffect::SpellAbsorption) + { + if(canAbsorb && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + absorbSpell(spellParams.getId(), caster, target); + return MWMechanics::MagicApplicationResult::REMOVED; + } + } + } + } + } + } + // Notify the target actor they've been hit + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) + target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + // Apply resistances + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) + { + const ESM::Spell* spell = nullptr; + if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellParams.getId()); + float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return MWMechanics::MagicApplicationResult::REMOVED; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + return MWMechanics::MagicApplicationResult::APPLIED; + } + static const std::map sBoundItemsMap{ {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, @@ -682,7 +774,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co } } -bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) { const auto world = MWBase::Environment::get().getWorld(); bool invalid = false; @@ -698,13 +790,13 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac } if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - return false; + return MagicApplicationResult::APPLIED; } else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) { if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - return true; + return MagicApplicationResult::REMOVED; } const auto* magicEffect = world->getStore().get().find(effect.mEffectId); if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) @@ -712,10 +804,10 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) { effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } else if(!dt) - return false; + return MagicApplicationResult::APPLIED; } if(effect.mEffectId == ESM::MagicEffect::Lock) { @@ -771,28 +863,19 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac } else { - auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); - if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) { - const ESM::Spell* spell = nullptr; - if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) - spell = world->getStore().get().search(spellParams.getId()); - float magnitudeMult = getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); - if (magnitudeMult == 0) - { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); - return true; - } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + MagicApplicationResult result = applyProtections(target, caster, spellParams, effect, magicEffect); + if(result != MagicApplicationResult::APPLIED) + return result; } float oldMagnitude = 0.f; if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) oldMagnitude = effect.mMagnitude; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + playEffects(target, *magicEffect); float magnitude = roll(effect); //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here effect.mMagnitude = magnitude; @@ -810,7 +893,7 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac effect.mMagnitude = oldMagnitude; effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } } if(effect.mEffectId == ESM::MagicEffect::Corprus) @@ -835,7 +918,7 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); if(recalculateMagicka) target.getClass().getCreatureStats(target).recalculateMagicka(); - return false; + return MagicApplicationResult::APPLIED; } void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp index a9e7b066f3..2861d0d64a 100644 --- a/apps/openmw/mwmechanics/spelleffects.hpp +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -10,8 +10,13 @@ namespace MWMechanics { + enum class MagicApplicationResult + { + APPLIED, REMOVED, REFLECTED + }; + // Applies a tick of a single effect. Returns true if the effect should be removed immediately - bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index a01128fe36..06debf4a97 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -104,8 +104,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; } else { @@ -172,8 +172,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) effect.mDuration = -1; effect.mTimeLeft = -1; effect.mEffectIndex = static_cast(effectIndex); - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; params.mEffects.emplace_back(effect); } auto [begin, end] = equippedItems.equal_range(id); diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 9ee137fab6..1f07a7ecee 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -546,7 +546,7 @@ namespace MWWorld cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; cast.mSlot = magicBoltState.mSlot; - cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); + cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, true); MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); magicBoltState.mToDelete = true; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ccd78170bb..a1501f1537 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3791,7 +3791,7 @@ namespace MWWorld cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; - cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); + cast.inflict(applyPair.first, caster, effectsToApply, rangeType, true); } } diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 8b5f1f1946..a79366f9c2 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -22,7 +22,9 @@ namespace ESM Flag_None = 0, Flag_Applied = 1 << 0, Flag_Remove = 1 << 1, - Flag_Ignore_Resistances = 1 << 2 + Flag_Ignore_Resistances = 1 << 2, + Flag_Ignore_Reflect = 1 << 3, + Flag_Ignore_SpellAbsorption = 1 << 4 }; int mEffectId; From 6aab2468790c456fe766b908f4fbdca0da617aec Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 26 Sep 2021 15:14:53 +0200 Subject: [PATCH 027/110] Add ESM records that are needed to store Lua scripts configuration; Use ptr.getType() (i.e. esm record names) instead of typeid(ptr.getClass()) in apps/openmw/mwlua. --- apps/openmw/mwlua/object.cpp | 83 ++++++++++++++++------------ apps/openmw/mwlua/object.hpp | 12 +++- apps/openmw/mwlua/objectbindings.cpp | 9 ++- components/esm/luascripts.cpp | 33 +++++++++-- components/esm/luascripts.hpp | 45 +++++++++++++-- 5 files changed, 129 insertions(+), 53 deletions(-) diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index 266e628dd6..69206e8c37 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -1,19 +1,6 @@ #include "object.hpp" -#include "../mwclass/activator.hpp" -#include "../mwclass/armor.hpp" -#include "../mwclass/book.hpp" -#include "../mwclass/clothing.hpp" -#include "../mwclass/container.hpp" -#include "../mwclass/creature.hpp" -#include "../mwclass/door.hpp" -#include "../mwclass/ingredient.hpp" -#include "../mwclass/light.hpp" -#include "../mwclass/misc.hpp" -#include "../mwclass/npc.hpp" -#include "../mwclass/potion.hpp" -#include "../mwclass/static.hpp" -#include "../mwclass/weapon.hpp" +#include namespace MWLua { @@ -23,28 +10,34 @@ namespace MWLua return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); } - const static std::map classNames = { - {typeid(MWClass::Activator), "Activator"}, - {typeid(MWClass::Armor), "Armor"}, - {typeid(MWClass::Book), "Book"}, - {typeid(MWClass::Clothing), "Clothing"}, - {typeid(MWClass::Container), "Container"}, - {typeid(MWClass::Creature), "Creature"}, - {typeid(MWClass::Door), "Door"}, - {typeid(MWClass::Ingredient), "Ingredient"}, - {typeid(MWClass::Light), "Light"}, - {typeid(MWClass::Miscellaneous), "Miscellaneous"}, - {typeid(MWClass::Npc), "NPC"}, - {typeid(MWClass::Potion), "Potion"}, - {typeid(MWClass::Static), "Static"}, - {typeid(MWClass::Weapon), "Weapon"}, + struct LuaObjectTypeInfo + { + std::string_view mName; + ESM::LuaScriptCfg::Flags mFlag = 0; }; - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback) + const static std::unordered_map luaObjectTypeInfo = { + {ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}}, + {ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}}, + {ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}}, + {ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}}, + {ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}}, + {ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}}, + {ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}}, + {ESM::REC_INGR, {"Ingredient", ESM::LuaScriptCfg::sIngredient}}, + {ESM::REC_LIGH, {"Light", ESM::LuaScriptCfg::sLight}}, + {ESM::REC_MISC, {"Miscellaneous", ESM::LuaScriptCfg::sMiscItem}}, + {ESM::REC_NPC_, {"NPC", ESM::LuaScriptCfg::sNPC}}, + {ESM::REC_ALCH, {"Potion", ESM::LuaScriptCfg::sPotion}}, + {ESM::REC_STAT, {"Static"}}, + {ESM::REC_WEAP, {"Weapon", ESM::LuaScriptCfg::sWeapon}}, + }; + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback) { - auto it = classNames.find(cls_type); - if (it != classNames.end()) - return it->second; + auto it = luaObjectTypeInfo.find(type); + if (it != luaObjectTypeInfo.end()) + return it->second.mName; else return fallback; } @@ -55,13 +48,31 @@ namespace MWLua return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; } - std::string_view getMWClassName(const MWWorld::Ptr& ptr) + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr) { + // Behaviour of this function is a part of OpenMW Lua API. We can not just return + // `ptr.getTypeDescription()` because its implementation is distributed over many files + // and can be accidentally changed. We use `ptr.getTypeDescription()` only as a fallback + // for types that are not present in `luaObjectTypeInfo` (for such types result stability + // is not necessary because they are not listed in OpenMW Lua documentation). if (ptr.getCellRef().getRefIdRef() == "player") return "Player"; if (isMarker(ptr)) return "Marker"; - return getMWClassName(typeid(ptr.getClass())); + return getLuaObjectTypeName(static_cast(ptr.getType()), /*fallback=*/ptr.getTypeDescription()); + } + + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr) + { + if (ptr.getCellRef().getRefIdRef() == "player") + return ESM::LuaScriptCfg::sPlayer; + if (isMarker(ptr)) + return 0; + auto it = luaObjectTypeInfo.find(static_cast(ptr.getType())); + if (it != luaObjectTypeInfo.end()) + return it->second.mFlag; + else + return 0; } std::string ptrToString(const MWWorld::Ptr& ptr) @@ -69,7 +80,7 @@ namespace MWLua std::string res = "object"; res.append(idToString(getId(ptr))); res.append(" ("); - res.append(getMWClassName(ptr)); + res.append(getLuaObjectTypeName(ptr)); res.append(", "); res.append(ptr.getCellRef().getRefIdRef()); res.append(")"); diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp index c0b6bf1919..5b1b5df74e 100644 --- a/apps/openmw/mwlua/object.hpp +++ b/apps/openmw/mwlua/object.hpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,8 +21,12 @@ namespace MWLua std::string idToString(const ObjectId& id); std::string ptrToString(const MWWorld::Ptr& ptr); bool isMarker(const MWWorld::Ptr& ptr); - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown"); - std::string_view getMWClassName(const MWWorld::Ptr& ptr); + std::string_view getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown"); + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr); + + // Each script has a set of flags that controls to which objects the script should be + // automatically attached. This function maps each object types to one of the flags. + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr); // Holds a mapping ObjectId -> MWWord::Ptr. class ObjectRegistry @@ -64,7 +70,7 @@ namespace MWLua ObjectId id() const { return mId; } std::string toString() const; - std::string_view type() const { return getMWClassName(ptr()); } + std::string_view type() const { return getLuaObjectTypeName(ptr()); } // Updates and returns the underlying Ptr. Throws an exception if object is not available. const MWWorld::Ptr& ptr() const; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index b7607c8b2c..07b7921a09 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -42,13 +42,12 @@ namespace MWLua template using Cell = std::conditional_t, LCell, GCell>; - template - static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr) + static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) { - if (typeid(Class) != typeid(ptr.getClass())) + if (ptr.getType() != recordType) { std::string msg = "Requires type '"; - msg.append(getMWClassName(typeid(Class))); + msg.append(getLuaObjectTypeName(recordType)); msg.append("', but applied to "); msg.append(ptrToString(ptr)); throw std::runtime_error(msg); @@ -189,7 +188,7 @@ namespace MWLua template static void addDoorBindings(sol::usertype& objectT, const Context& context) { - auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass(o.ptr()); }; + auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireRecord(ESM::REC_DOOR, o.ptr()); }; objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o) { diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 7ff841312a..c831cbbbfc 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -5,13 +5,15 @@ // List of all records, that are related to Lua. // -// Record: -// LUAM - MWLua::LuaManager +// Records: +// LUAL - LuaScriptsCfg - list of all scripts (in content files) +// LUAM - MWLua::LuaManager (in saves) // // Subrecords: +// LUAF - LuaScriptCfg::mFlags // LUAW - Start of MWLua::WorldView data // LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) -// LUAS - Start LuaUtil::ScriptsContainer data (scriptName) +// LUAS - VFS path to a Lua script // LUAD - Serialized Lua variable // LUAT - MWLua::ScriptsContainer::Timer // LUAC - Name of a timer callback (string) @@ -37,6 +39,28 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm) return data; } +void ESM::LuaScriptsCfg::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + uint64_t flags; + esm.getHNT(flags, "LUAF"); + std::string data = loadLuaBinaryData(esm); + mScripts.push_back({std::move(name), std::move(data), flags}); + } +} + +void ESM::LuaScriptsCfg::save(ESMWriter& esm) const +{ + for (const LuaScriptCfg& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + esm.writeHNT("LUAF", script.mFlags); + saveLuaBinaryData(esm, script.mInitializationData); + } +} + void ESM::LuaScripts::load(ESMReader& esm) { while (esm.isNextSub("LUAS")) @@ -63,8 +87,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const for (const LuaScript& script : mScripts) { esm.writeHNString("LUAS", script.mScriptPath); - if (!script.mData.empty()) - saveLuaBinaryData(esm, script.mData); + saveLuaBinaryData(esm, script.mData); for (const LuaTimer& timer : script.mTimers) { esm.startSubRecord("LUAT"); diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index f268f41536..e6f7113c16 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -9,7 +9,44 @@ namespace ESM class ESMReader; class ESMWriter; - // Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record. + // LuaScriptCfg, LuaScriptsCfg are used in content files. + + struct LuaScriptCfg + { + using Flags = uint64_t; + static constexpr Flags sGlobal = 1ull << 0; + static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script + static constexpr Flags sPlayer = 1ull << 2; // auto attach to players + // auto attach for other classes: + static constexpr Flags sActivator = 1ull << 3; + static constexpr Flags sArmor = 1ull << 4; + static constexpr Flags sBook = 1ull << 5; + static constexpr Flags sClothing = 1ull << 6; + static constexpr Flags sContainer = 1ull << 7; + static constexpr Flags sCreature = 1ull << 8; + static constexpr Flags sDoor = 1ull << 9; + static constexpr Flags sIngredient = 1ull << 10; + static constexpr Flags sLight = 1ull << 11; + static constexpr Flags sMiscItem = 1ull << 12; + static constexpr Flags sNPC = 1ull << 13; + static constexpr Flags sPotion = 1ull << 14; + static constexpr Flags sWeapon = 1ull << 15; + + std::string mScriptPath; + std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'. + Flags mFlags; // bitwise OR of Flags. + }; + + struct LuaScriptsCfg + { + std::vector mScripts; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + // LuaTimer, LuaScript, LuaScripts are used in saved game files. + // Storage structure for LuaUtil::ScriptsContainer. These are not top-level records. // Used either for global scripts or for local scripts on a specific object. struct LuaTimer @@ -37,11 +74,11 @@ namespace ESM { std::vector mScripts; - void load (ESMReader &esm); - void save (ESMWriter &esm) const; + void load(ESMReader &esm); + void save(ESMWriter &esm) const; }; - // Saves binary string `data` (can contain '\0') as record LUAD. + // Saves binary string `data` (can contain '\0') as LUAD record. void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. From 33d71be81f8254c24de685eee3c0ba7cf93ca005 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Wed, 6 Oct 2021 20:59:48 +0200 Subject: [PATCH 028/110] Add LuaUtil::ScriptsConfiguration --- apps/openmw_test_suite/CMakeLists.txt | 2 +- .../lua/test_configuration.cpp | 58 ++++++ apps/openmw_test_suite/lua/testing_util.hpp | 2 +- components/lua/configuration.cpp | 165 ++++++++++++++++++ components/lua/configuration.hpp | 37 ++++ components/misc/stringops.hpp | 21 ++- 6 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 apps/openmw_test_suite/lua/test_configuration.cpp create mode 100644 components/lua/configuration.cpp create mode 100644 components/lua/configuration.hpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index a2a35e3aab..2b96d4c633 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -20,7 +20,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_utilpackage.cpp lua/test_serialization.cpp lua/test_querypackage.cpp - lua/test_omwscriptsparser.cpp + lua/test_configuration.cpp misc/test_stringops.cpp misc/test_endianness.cpp diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp new file mode 100644 index 0000000000..054ea8cbda --- /dev/null +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -0,0 +1,58 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + + TEST(LuaConfigurationTest, ValidConfiguration) + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( + # Lines starting with '#' are comments + GLOBAL: my_mod/#some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER :my_mod/player.lua + CUSTOM : my_mod/some_other_script.lua + NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n"); + + ASSERT_EQ(cfg.mScripts.size(), 6); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua"); + + LuaUtil::ScriptsConfiguration conf; + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 3); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); + // cfg.mScripts[1] is overridden by cfg.mScripts[4] + // cfg.mScripts[2] is overridden by cfg.mScripts[3] + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + // cfg.mScripts[4] is removed because there are no flags + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua"); + + cfg = ESM::LuaScriptsCfg(); + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 0); + } + + TEST(LuaConfigurationTest, Errors) + { + ESM::LuaScriptsCfg cfg; + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), + "Lua script should have suffix '.lua', got: GLOBAL: something"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), + "No flags found in: something.lua"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"), + "Global script can not have local flags"); + } + +} diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp index 28c4d59930..2f6810350f 100644 --- a/apps/openmw_test_suite/lua/testing_util.hpp +++ b/apps/openmw_test_suite/lua/testing_util.hpp @@ -52,7 +52,7 @@ namespace } #define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \ - catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); } + catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); } } diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp new file mode 100644 index 0000000000..4598ed2508 --- /dev/null +++ b/components/lua/configuration.cpp @@ -0,0 +1,165 @@ +#include "configuration.hpp" + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + + namespace + { + const std::map> flagsByName{ + {"GLOBAL", ESM::LuaScriptCfg::sGlobal}, + {"CUSTOM", ESM::LuaScriptCfg::sCustom}, + {"PLAYER", ESM::LuaScriptCfg::sPlayer}, + {"ACTIVATOR", ESM::LuaScriptCfg::sActivator}, + {"ARMOR", ESM::LuaScriptCfg::sArmor}, + {"BOOK", ESM::LuaScriptCfg::sBook}, + {"CLOTHING", ESM::LuaScriptCfg::sClothing}, + {"CONTAINER", ESM::LuaScriptCfg::sContainer}, + {"CREATURE", ESM::LuaScriptCfg::sCreature}, + {"DOOR", ESM::LuaScriptCfg::sDoor}, + {"INGREDIENT", ESM::LuaScriptCfg::sIngredient}, + {"LIGHT", ESM::LuaScriptCfg::sLight}, + {"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem}, + {"NPC", ESM::LuaScriptCfg::sNPC}, + {"POTION", ESM::LuaScriptCfg::sPotion}, + {"WEAPON", ESM::LuaScriptCfg::sWeapon}, + }; + } + + const std::vector ScriptsConfiguration::sEmpty; + + void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg) + { + mScripts.clear(); + mScriptsByFlag.clear(); + mPathToIndex.clear(); + + // Find duplicates; only the last occurrence will be used. + // Search for duplicates is case insensitive. + std::vector skip(cfg.mScripts.size(), false); + for (int i = cfg.mScripts.size() - 1; i >= 0; --i) + { + auto [_, inserted] = mPathToIndex.insert_or_assign( + Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1); + if (!inserted || cfg.mScripts[i].mFlags == 0) + skip[i] = true; + } + mPathToIndex.clear(); + int index = 0; + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + if (skip[i]) + continue; + ESM::LuaScriptCfg& s = cfg.mScripts[i]; + mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive. + ESM::LuaScriptCfg::Flags flags = s.mFlags; + ESM::LuaScriptCfg::Flags flag = 1; + while (flags != 0) + { + if (flags & flag) + mScriptsByFlag[flag].push_back(index); + flags &= ~flag; + flag = flag << 1; + } + mScripts.push_back(std::move(s)); + index++; + } + } + + std::optional ScriptsConfiguration::findId(std::string_view path) const + { + auto it = mPathToIndex.find(path); + if (it != mPathToIndex.end()) + return it->second; + else + return std::nullopt; + } + + const std::vector& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const + { + assert(std::bitset<64>(type).count() <= 1); + auto it = mScriptsByFlag.find(type); + if (it != mScriptsByFlag.end()) + return it->second; + else + return sEmpty; + } + + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data) + { + while (!data.empty()) + { + // Get next line + std::string_view line = data.substr(0, data.find('\n')); + data = data.substr(std::min(line.size() + 1, data.size())); + if (!line.empty() && line.back() == '\r') + line = line.substr(0, line.size() - 1); + + while (!line.empty() && std::isspace(line[0])) + line = line.substr(1); + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + while (!line.empty() && std::isspace(line.back())) + line = line.substr(0, line.size() - 1); + + if (!Misc::StringUtils::ciEndsWith(line, ".lua")) + throw std::runtime_error(Misc::StringUtils::format( + "Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300)))); + + // Split flags and script path + size_t semicolonPos = line.find(':'); + if (semicolonPos == std::string::npos) + throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line))); + std::string_view flagsStr = line.substr(0, semicolonPos); + std::string_view scriptPath = line.substr(semicolonPos + 1); + while (std::isspace(scriptPath[0])) + scriptPath = scriptPath.substr(1); + + // Parse flags + ESM::LuaScriptCfg::Flags flags = 0; + size_t flagsPos = 0; + while (true) + { + while (flagsPos < flagsStr.size() && (std::isspace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ',')) + flagsPos++; + size_t startPos = flagsPos; + while (flagsPos < flagsStr.size() && !std::isspace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',') + flagsPos++; + if (startPos == flagsPos) + break; + std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos); + auto it = flagsByName.find(flagName); + if (it != flagsByName.end()) + flags |= it->second; + else + throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s", + std::string(flagName), std::string(line))); + } + if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal) + throw std::runtime_error("Global script can not have local flags"); + + cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags}); + } + } + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script) + { + std::stringstream ss; + for (const auto& [flagName, flag] : flagsByName) + { + if (script.mFlags & flag) + ss << flagName << " "; + } + ss << ": " << script.mScriptPath; + if (!script.mInitializationData.empty()) + ss << " (with data, " << script.mInitializationData.size() << " bytes)"; + return ss.str(); + } + +} diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp new file mode 100644 index 0000000000..32eddf399c --- /dev/null +++ b/components/lua/configuration.hpp @@ -0,0 +1,37 @@ +#ifndef COMPONENTS_LUA_CONFIGURATION_H +#define COMPONENTS_LUA_CONFIGURATION_H + +#include +#include + +#include + +namespace LuaUtil +{ + + class ScriptsConfiguration + { + public: + void init(ESM::LuaScriptsCfg); + + size_t size() const { return mScripts.size(); } + const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; } + + std::optional findId(std::string_view path) const; + const std::vector& getListByFlag(ESM::LuaScriptCfg::Flags type) const; + + private: + std::vector mScripts; + std::map> mPathToIndex; + std::map> mScriptsByFlag; + static const std::vector sEmpty; + }; + + // Parse ESM::LuaScriptsCfg from text and add to `cfg`. + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data); + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script); + +} + +#endif // COMPONENTS_LUA_CONFIGURATION_H diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 6c9c1eefa2..dcffa7fdf3 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -25,6 +25,7 @@ class StringUtils template static T argument(T value) noexcept { + static_assert(!std::is_same_v, "std::string_view is not supported"); return value; } @@ -324,14 +325,20 @@ public: } } - static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) - { - size_t pos = str.rfind(substr); - if (pos == std::string::npos) - return; + static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) + { + size_t pos = str.rfind(substr); + if (pos == std::string::npos) + return; - str.replace(pos, substr.size(), with); - } + str.replace(pos, substr.size(), with); + } + + static inline bool ciEndsWith(std::string_view s, std::string_view suffix) + { + return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(), + [](char l, char r) { return toLower(l) == toLower(r); }); + }; }; } From 9adc1902092f32b8dc79c193f3063f72c207cb09 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 25 Sep 2021 10:46:47 +0200 Subject: [PATCH 029/110] Redesign LuaUtil::ScriptsContainer to work with ScriptsConfiguration --- apps/openmw/mwbase/luamanager.hpp | 1 + apps/openmw/mwlua/asyncbindings.cpp | 12 +- apps/openmw/mwlua/globalscripts.hpp | 3 +- apps/openmw/mwlua/localscripts.cpp | 6 +- apps/openmw/mwlua/localscripts.hpp | 4 +- apps/openmw/mwlua/luabindings.cpp | 2 +- apps/openmw/mwlua/luabindings.hpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 100 +++- apps/openmw/mwlua/luamanagerimp.hpp | 21 +- apps/openmw/mwlua/objectbindings.cpp | 38 +- apps/openmw/mwlua/playerscripts.hpp | 2 +- apps/openmw/mwstate/statemanagerimp.cpp | 2 + apps/openmw_test_suite/lua/test_lua.cpp | 5 +- .../lua/test_omwscriptsparser.cpp | 59 --- .../lua/test_scriptscontainer.cpp | 197 +++++--- components/CMakeLists.txt | 2 +- components/lua/luastate.cpp | 13 +- components/lua/luastate.hpp | 21 +- components/lua/omwscriptsparser.cpp | 44 -- components/lua/omwscriptsparser.hpp | 14 - components/lua/scriptscontainer.cpp | 451 +++++++++++------- components/lua/scriptscontainer.hpp | 105 ++-- 22 files changed, 638 insertions(+), 466 deletions(-) delete mode 100644 apps/openmw_test_suite/lua/test_omwscriptsparser.cpp delete mode 100644 components/lua/omwscriptsparser.cpp delete mode 100644 components/lua/omwscriptsparser.hpp diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index ebcd8f50b3..cc479a2937 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -30,6 +30,7 @@ namespace MWBase virtual ~LuaManager() = default; virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; virtual void registerObject(const MWWorld::Ptr& ptr) = 0; virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 9fdda53d9d..d438518452 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -23,7 +23,7 @@ namespace MWLua sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) { - asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback)); + asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); return TimerCallback{asyncId, std::string(name)}; }; api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, @@ -31,24 +31,24 @@ namespace MWLua { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback)); }; api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback)); }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { @@ -59,7 +59,7 @@ namespace MWLua { LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); - return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; + return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 9a371809ac..2737dabaca 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -16,7 +16,8 @@ namespace MWLua class GlobalScripts : public LuaUtil::ScriptsContainer { public: - GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + GlobalScripts(LuaUtil::LuaState* lua) : + LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal) { registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8a1b76a8ce..ee23b4b90c 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -82,14 +82,14 @@ namespace MWLua }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) - : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj) { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); } - void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) + void LocalScripts::receiveEngineEvent(const EngineEvent& event) { std::visit([this](auto&& arg) { diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 80d04b7a40..68da0b8b03 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } @@ -39,7 +39,7 @@ namespace MWLua }; using EngineEvent = std::variant; - void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); + void receiveEngineEvent(const EngineEvent&); protected: SelfObject mData; diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index aceffc24db..1c05debc73 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -25,7 +25,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 7; + api["API_REVISION"] = 8; api["quit"] = [lua]() { std::string traceback = lua->sol()["debug"]["traceback"]().get(); diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index d1c62e43e3..aad3183734 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -48,7 +48,7 @@ namespace MWLua struct AsyncPackageId { LuaUtil::ScriptsContainer* mContainer; - std::string mScript; + int mScriptId; sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 38055c99b7..7a4b319f27 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "../mwbase/windowmanager.hpp" @@ -20,10 +19,10 @@ namespace MWLua { - LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector& scriptLists) : mLua(vfs) + LuaManager::LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles) : + mVFS(vfs), mOMWScriptsFiles(std::move(OMWScriptsFiles)), mLua(vfs, &mConfiguration) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); - mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); @@ -33,6 +32,30 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + void LuaManager::initConfiguration() + { + ESM::LuaScriptsCfg cfg; + for (const std::string& file : mOMWScriptsFiles) + { + if (!Misc::StringUtils::endsWith(file, ".omwscripts")) + { + Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << file << "'"; + continue; + } + try + { + std::string content(std::istreambuf_iterator(*mVFS->get(file)), {}); + LuaUtil::parseOMWScripts(cfg, content); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + // TODO: Add data from content files + mConfiguration.init(cfg); + Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; + for (size_t i = 0; i < mConfiguration.size(); ++i) + Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + } + void LuaManager::init() { Context context; @@ -67,10 +90,7 @@ namespace MWLua mLocalSettingsPackage = initLocalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); - mInputEvents.clear(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script started: " << path; + initConfiguration(); mInitialized = true; } @@ -160,7 +180,7 @@ namespace MWLua } LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) - scripts->receiveEngineEvent(e.mEvent, objectRegistry); + scripts->receiveEngineEvent(e.mEvent); } mLocalEngineEvents.clear(); @@ -173,6 +193,11 @@ namespace MWLua mPlayerChanged = false; mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); } + if (mNewGameStarted) + { + mNewGameStarted = false; + mGlobalScripts.newGameStarted(); + } for (ObjectId id : mActorAddedEvents) mGlobalScripts.actorActive(GObject(id, objectRegistry)); @@ -205,8 +230,11 @@ namespace MWLua mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); + mNewGameStarted = false; mPlayerChanged = false; mWorldView.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); @@ -225,17 +253,38 @@ namespace MWLua mPlayer = ptr; LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); mActiveLocalScripts.insert(localScripts); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); mPlayerChanged = true; } + void LuaManager::newGameStarted() + { + mNewGameStarted = true; + mInputEvents.clear(); + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + + void LuaManager::gameLoaded() + { + if (!mGlobalScriptsStarted) + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr); + if (!mConfiguration.getListByFlag(flag).empty()) + localScripts = createLocalScripts(ptr, flag); // TODO: put to a queue and apply on next `update()` + } if (localScripts) { mActiveLocalScripts.insert(localScripts); @@ -281,26 +330,26 @@ namespace MWLua return localScripts->getActorControls(); } - void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath) + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); } - localScripts->addNewScript(scriptPath); + localScripts->addCustomScript(scriptId); } - LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) { assert(mInitialized); + assert(flag != ESM::LuaScriptCfg::sGlobal); std::shared_ptr scripts; - // When loading a game, it can be called before LuaManager::setPlayer, - // so we can't just check ptr == mPlayer here. - if (ptr.getCellRef().getRefIdRef() == "player") + if (flag == ESM::LuaScriptCfg::sPlayer) { + assert(ptr.getCellRef().getRefIdRef() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); @@ -309,11 +358,12 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); + scripts->addAutoStartedScripts(); MWWorld::RefData& refData = ptr.getRefData(); refData.setLuaScripts(std::move(scripts)); @@ -344,8 +394,9 @@ namespace MWLua loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get()); - mGlobalScripts.load(globalScripts, false); + mGlobalScripts.load(globalScripts); mGlobalScripts.setSerializer(mGlobalSerializer.get()); + mGlobalScriptsStarted = true; } void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) @@ -366,10 +417,10 @@ namespace MWLua } mWorldView.getObjectRegistry()->registerPtr(ptr); - LocalScripts* scripts = createLocalScripts(ptr); + LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); scripts->setSerializer(mLocalLoader.get()); - scripts->load(data, true); + scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. @@ -380,15 +431,12 @@ namespace MWLua { Log(Debug::Info) << "Reload Lua"; mLua.dropScriptCache(); + initConfiguration(); { // Reload global scripts ESM::LuaScripts data; mGlobalScripts.save(data); - mGlobalScripts.removeAllScripts(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script restarted: " << path; - mGlobalScripts.load(data, false); + mGlobalScripts.load(data); } for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) @@ -398,7 +446,7 @@ namespace MWLua continue; ESM::LuaScripts data; scripts->save(data); - scripts->load(data, true); + scripts->load(data); } } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 91f48171f3..be80cfe2e7 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -35,9 +35,9 @@ namespace MWLua class LuaManager : public MWBase::LuaManager { public: - LuaManager(const VFS::Manager* vfs, const std::vector& globalScriptLists); + LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles); - // Called by engine.cpp when environment is fully initialized. + // Called by engine.cpp when the environment is fully initialized. void init(); // Called by engine.cpp every frame. For performance reasons it works in a separate @@ -49,7 +49,8 @@ namespace MWLua // Available everywhere through the MWBase::LuaManager interface. // LuaManager queues these events and propagates to scripts on the next `update` call. - void newGameStarted() override { mGlobalScripts.newGameStarted(); } + void newGameStarted() override; + void gameLoaded() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void registerObject(const MWWorld::Ptr& ptr) override; @@ -62,8 +63,8 @@ namespace MWLua void clear() override; // should be called before loading game or starting a new game to reset internal state. void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". - // Used only in luabindings - void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } @@ -93,9 +94,15 @@ namespace MWLua } private: - LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); + + const VFS::Manager* mVFS; + const std::vector mOMWScriptsFiles; bool mInitialized = false; + bool mGlobalScriptsStarted = false; + LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; sol::table mNearbyPackage; sol::table mUserInterfacePackage; @@ -104,12 +111,12 @@ namespace MWLua sol::table mLocalSettingsPackage; sol::table mPlayerSettingsPackage; - std::vector mGlobalScriptList; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; WorldView mWorldView; bool mPlayerChanged = false; + bool mNewGameStarted = false; MWWorld::Ptr mPlayer; GlobalEventQueue mGlobalEvents; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 07b7921a09..2eceecc061 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -140,9 +140,43 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) + objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path) { - luaManager->addLocalScript(object.ptr(), path); + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + luaManager->addCustomLocalScript(object.ptr(), *scriptId); + }; + objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); + ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags; + if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); }; objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index ff0349b3c6..0393a1375d 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -13,7 +13,7 @@ namespace MWLua class PlayerScripts : public LocalScripts { public: - PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) { registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4387d7faa7..5e51e2f621 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -558,6 +558,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); } catch (const std::exception& e) { diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 32d4ea49b8..4b3ecdcb2b 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -58,7 +58,8 @@ return { {"invalid.lua", &invalidScriptFile} }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; }; TEST_F(LuaStateTest, Sandbox) @@ -148,7 +149,7 @@ return { TEST_F(LuaStateTest, ProvideAPI) { - LuaUtil::LuaState lua(mVFS.get()); + LuaUtil::LuaState lua(mVFS.get(), &mCfg); sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); diff --git a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp deleted file mode 100644 index b1526ef9b6..0000000000 --- a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "gmock/gmock.h" -#include - -#include - -#include "testing_util.hpp" - -namespace -{ - using namespace testing; - - TestFile file1( - "#comment.lua\n" - "\n" - "script1.lua\n" - "some mod/Some Script.lua" - ); - TestFile file2( - "#comment.lua\r\n" - "\r\n" - "script2.lua\r\n" - "some other mod/Some Script.lua\r" - ); - TestFile emptyFile(""); - TestFile invalidFile("Invalid file"); - - struct OMWScriptsParserTest : Test - { - std::unique_ptr mVFS = createTestVFS({ - {"file1.omwscripts", &file1}, - {"file2.omwscripts", &file2}, - {"empty.omwscripts", &emptyFile}, - {"invalid.lua", &file1}, - {"invalid.omwscripts", &invalidFile}, - }); - }; - - TEST_F(OMWScriptsParserTest, Basic) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), ""); - EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua", - "script1.lua", "some mod/Some Script.lua")); - } - - TEST_F(OMWScriptsParserTest, InvalidFiles) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"invalid.lua", "invalid.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), - "Script list should have suffix '.omwscripts', got: 'invalid.lua'\n" - "Lua script should have suffix '.lua', got: 'Invalid file'\n"); - EXPECT_THAT(res, ElementsAre()); - } - -} diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 8f05138782..344fbb3c78 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -18,7 +18,10 @@ namespace TestFile testScript(R"X( return { - engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, eventHandlers = { Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, @@ -75,15 +78,25 @@ return { )X"); TestFile overrideInterfaceScript(R"X( -local old = require('openmw.interfaces').TestInterface +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} return { interfaceName = "TestInterface", - interface = { - fn = function(x) - print('NEW FN', x) - old.fn(x) - end, - value = old.value + 1 + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end }, } )X"); @@ -115,7 +128,25 @@ return { {"useInterface.lua", &useInterfaceScript}, }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + mCfg.init(std::move(cfg)); + } }; TEST_F(LuaScriptsContainerTest, VerifyStructure) @@ -123,21 +154,21 @@ return { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); { testing::internal::CaptureStdout(); - EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("empty.lua")); - EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present EXPECT_EQ(internal::GetCapturedStdout(), ""); } } @@ -146,9 +177,9 @@ return { { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" "Test[test2.lua]:\t update 1.5\n"); @@ -157,9 +188,9 @@ return { TEST_F(LuaScriptsContainerTest, CallEvent) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); @@ -204,9 +235,9 @@ return { TEST_F(LuaScriptsContainerTest, RemoveScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); { @@ -221,8 +252,10 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); - EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -233,7 +266,7 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.removeScript(*mCfg.findId("test1.lua")); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -242,17 +275,41 @@ return { } } - TEST_F(LuaScriptsContainerTest, Interface) + TEST_F(LuaScriptsContainerTest, AutoStart) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); - scripts.update(1.5f); - EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + scripts.addAutoStartedScripts(); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n"); + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature); + int addIfaceId = *mCfg.findId("testInterface.lua"); + int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); + int useIfaceId = *mCfg.findId("useInterface.lua"); + + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + scripts.removeScript(overrideIfaceId); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" "Test[testInterface.lua]:\tFN\t4.5\n" "Test[testInterface.lua]:\tFN\t3.5\n"); @@ -260,16 +317,12 @@ return { TEST_F(LuaScriptsContainerTest, LoadSave) { - LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); - EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); - EXPECT_TRUE(scripts1.addNewScript("test1.lua")); - EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); - - EXPECT_TRUE(scripts3.addNewScript("test2.lua")); - EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( "n", 1, @@ -282,23 +335,30 @@ return { ESM::LuaScripts data; scripts1.save(data); - scripts2.load(data, true); - scripts3.load(data, false); { testing::internal::CaptureStdout(); + scripts2.load(data); scripts2.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\tload\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test1.lua]:\tprint\n" - "Test[loadSave1.lua]:\t2.5\t1.5\n"); + "Test[loadSave1.lua]:\t2.5\t1.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); } { testing::internal::CaptureStdout(); + scripts3.load(data); scripts3.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Ignoring Test[loadSave1.lua]; this script is not allowed here\n" + "Test[test1.lua]:\tload\n" + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test2.lua]:\tprint\n"); + "Test[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); } } @@ -306,8 +366,13 @@ return { { using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + int test1Id = *mCfg.findId("test1.lua"); + int test2Id = *mCfg.findId("test2.lua"); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + EXPECT_EQ(internal::GetCapturedStdout(), ""); int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); @@ -315,25 +380,25 @@ return { sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); - scripts.registerTimerCallback("test1.lua", "A", fn3); - scripts.registerTimerCallback("test1.lua", "B", fn4); - scripts.registerTimerCallback("test2.lua", "B", fn3); - scripts.registerTimerCallback("test2.lua", "A", fn4); + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1); EXPECT_EQ(counter1, 0); EXPECT_EQ(counter3, 0); @@ -358,10 +423,12 @@ return { EXPECT_EQ(counter3, 5); EXPECT_EQ(counter4, 5); + testing::internal::CaptureStdout(); ESM::LuaScripts data; scripts.save(data); - scripts.load(data, true); - scripts.registerTimerCallback("test1.lua", "B", fn4); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); testing::internal::CaptureStdout(); scripts.processTimers(20, 20); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7e1abe0a4e..a3f77d86bf 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate scriptscontainer utilpackage serialization omwscriptsparser + luastate scriptscontainer utilpackage serialization configuration ) add_component_dir (settings diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 8e4719dba4..e78f7bed06 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -22,7 +22,7 @@ namespace LuaUtil "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"}; static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; - LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) { mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table, sol::lib::debug); @@ -95,12 +95,11 @@ namespace LuaUtil return res; } - void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package) + void LuaState::addCommonPackage(std::string packageName, sol::object package) { - if (package.is()) - mCommonPackages[packageName] = package; - else - mCommonPackages[packageName] = makeReadOnly(package); + if (!package.is()) + package = makeReadOnly(std::move(package)); + mCommonPackages.emplace(std::move(packageName), std::move(package)); } sol::protected_function_result LuaState::runInNewSandbox( @@ -148,7 +147,7 @@ namespace LuaUtil return std::move(res); } - sol::protected_function LuaState::loadScript(const std::string& path) + sol::function LuaState::loadScript(const std::string& path) { auto iter = mCompiledScripts.find(path); if (iter != mCompiledScripts.end()) diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 8982b49b36..7ac5af0b1b 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -7,6 +7,8 @@ #include +#include "configuration.hpp" + namespace LuaUtil { @@ -22,12 +24,12 @@ namespace LuaUtil // - Access to common read-only resources from different sandboxes; // - Replace standard `require` with a safe version that allows to search // Lua libraries (only source, no dll's) in the virtual filesystem; - // - Make `print` to add the script name to the every message and - // write to Log rather than directly to stdout; + // - Make `print` to add the script name to every message and + // write to the Log rather than directly to stdout; class LuaState { public: - explicit LuaState(const VFS::Manager* vfs); + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); ~LuaState(); // Returns underlying sol::state. @@ -40,7 +42,7 @@ namespace LuaUtil // The package can be either a sol::table with an API or a sol::function. If it is a function, // it will be evaluated (once per sandbox) the first time when requested. If the package // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). - void addCommonPackage(const std::string& packageName, const sol::object& package); + void addCommonPackage(std::string packageName, sol::object package); // Creates a new sandbox, runs a script, and returns the result // (the result is expected to be an interface of the script). @@ -58,14 +60,17 @@ namespace LuaUtil void dropScriptCache() { mCompiledScripts.clear(); } + const ScriptsConfiguration& getConfiguration() const { return *mConf; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template - friend sol::protected_function_result call(sol::protected_function fn, Args&&... args); + friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); - sol::protected_function loadScript(const std::string& path); + sol::function loadScript(const std::string& path); sol::state mLua; + const ScriptsConfiguration* mConf; sol::table mSandboxEnv; std::map mCompiledScripts; std::map mCommonPackages; @@ -75,7 +80,7 @@ namespace LuaUtil // Should be used for every call of every Lua function. // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 template - sol::protected_function_result call(sol::protected_function fn, Args&&... args) + sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) { try { @@ -101,7 +106,7 @@ namespace LuaUtil std::string toString(const sol::object&); // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. - // Needed to forbid any changes in common resources that can accessed from different sandboxes. + // Needed to forbid any changes in common resources that can be accessed from different sandboxes. sol::table makeReadOnly(sol::table); sol::table getMutableFromReadOnly(const sol::userdata&); diff --git a/components/lua/omwscriptsparser.cpp b/components/lua/omwscriptsparser.cpp deleted file mode 100644 index bc73e013db..0000000000 --- a/components/lua/omwscriptsparser.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "omwscriptsparser.hpp" - -#include - -#include - -std::vector LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists) -{ - auto endsWith = [](std::string_view s, std::string_view suffix) - { - return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); - }; - std::vector res; - for (const std::string& scriptListFile : scriptLists) - { - if (!endsWith(scriptListFile, ".omwscripts")) - { - Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'"; - continue; - } - std::string content(std::istreambuf_iterator(*vfs->get(scriptListFile)), {}); - std::string_view view(content); - while (!view.empty()) - { - size_t pos = 0; - while (pos < view.size() && view[pos] != '\n') - pos++; - std::string_view line = view.substr(0, pos); - view = view.substr(std::min(pos + 1, view.size())); - if (!line.empty() && line.back() == '\r') - line = line.substr(0, pos - 1); - // Lines starting with '#' are comments. - // TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua' - // is a valid path, but '#path/to/file.lua' is considered as a comment and ignored. - if (line.empty() || line[0] == '#') - continue; - if (endsWith(line, ".lua")) - res.push_back(std::string(line)); - else - Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'"; - } - } - return res; -} diff --git a/components/lua/omwscriptsparser.hpp b/components/lua/omwscriptsparser.hpp deleted file mode 100644 index 1da9f123b2..0000000000 --- a/components/lua/omwscriptsparser.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H -#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H - -#include - -namespace LuaUtil -{ - - // Parses list of `*.omwscripts` files. - std::vector parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists); - -} - -#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 703381a453..f4922a8b6b 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -10,8 +10,10 @@ namespace LuaUtil static constexpr std::string_view INTERFACE_NAME = "interfaceName"; static constexpr std::string_view INTERFACE = "interface"; + static constexpr std::string_view HANDLER_INIT = "onInit"; static constexpr std::string_view HANDLER_SAVE = "onSave"; static constexpr std::string_view HANDLER_LOAD = "onLoad"; + static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; @@ -25,147 +27,238 @@ namespace LuaUtil return res; } - ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode) + : mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode) { registerEngineHandlers({&mUpdateHandlers}); mPublicInterfaces = sol::table(lua->sol(), sol::create); addPackage("openmw.interfaces", mPublicInterfaces); } - void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) { - API[packageName] = makeReadOnly(std::move(package)); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } - bool ScriptsContainer::addNewScript(const std::string& path) + void ScriptsContainer::addPackage(std::string packageName, sol::object package) { - if (mScripts.count(path) != 0) + mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package))); + } + + bool ScriptsContainer::addCustomScript(int scriptId) + { + assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom); + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + return ok; + } + + void ScriptsContainer::addAutoStartedScripts() + { + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + { + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + } + } + + bool ScriptsContainer::addScript(int scriptId, std::optional& onInit, std::optional& onLoad) + { + assert(scriptId >= 0 && scriptId < static_cast(mLua.getConfiguration().size())); + if (mScripts.count(scriptId) != 0) return false; // already present + const std::string& path = scriptPath(scriptId); try { - sol::table hiddenData(mLua.sol(), sol::create); - hiddenData[ScriptId::KEY] = ScriptId{this, path}; - hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); - hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); - mScripts[path].mHiddenData = hiddenData; - sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); - std::string interfaceName = ""; - sol::object publicInterface = sol::nil; - if (script != sol::nil) + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[ScriptId::KEY] = ScriptId{this, scriptId, path}; + script.mHiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); + script.mHiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); + sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); + if (scriptOutput == sol::nil) + return true; + sol::object engineHandlers = sol::nil, eventHandlers = sol::nil; + for (const auto& [key, value] : sol::table(scriptOutput)) { - for (auto& [key, value] : sol::table(script)) + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + engineHandlers = value; + else if (sectionName == EVENT_HANDLERS) + eventHandlers = value; + else if (sectionName == INTERFACE_NAME) + script.mInterfaceName = value.as(); + else if (sectionName == INTERFACE) + script.mInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + } + if (engineHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(engineHandlers)) { - std::string_view sectionName = key.as(); - if (sectionName == ENGINE_HANDLERS) - parseEngineHandlers(value, path); - else if (sectionName == EVENT_HANDLERS) - parseEventHandlers(value, path); - else if (sectionName == INTERFACE_NAME) - interfaceName = value.as(); - else if (sectionName == INTERFACE) - publicInterface = value.as(); + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_INIT) + onInit = sol::function(fn); + else if (handlerName == HANDLER_LOAD) + onLoad = sol::function(fn); + else if (handlerName == HANDLER_SAVE) + script.mOnSave = sol::function(fn); + else if (handlerName == HANDLER_INTERFACE_OVERRIDE) + script.mOnOverride = sol::function(fn); else - Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + { + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName + << "' in " << mNamePrefix << "[" << path << "]"; + else + insertHandler(it->second->mList, scriptId, fn); + } } } - if (interfaceName.empty() != (publicInterface == sol::nil)) + if (eventHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(eventHandlers)) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first; + insertHandler(it->second, scriptId, fn); + } + } + + if (script.mInterfaceName.empty() == script.mInterface.has_value()) + { Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; - else if (!interfaceName.empty()) - script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); - mScriptOrder.push_back(path); - mScripts[path].mInterface = std::move(script); + script.mInterfaceName.clear(); + script.mInterface = sol::nil; + } + else if (script.mInterface) + { + script.mInterface = makeReadOnly(*script.mInterface); + insertInterface(scriptId, script); + } + return true; } catch (std::exception& e) { - mScripts.erase(path); + mScripts.erase(scriptId); Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); return false; } } - bool ScriptsContainer::removeScript(const std::string& path) + void ScriptsContainer::removeScript(int scriptId) { - auto scriptIter = mScripts.find(path); + auto scriptIter = mScripts.find(scriptId); if (scriptIter == mScripts.end()) - return false; // no such script - scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil; - sol::object& script = scriptIter->second.mInterface; - if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) - { - std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); - if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) - { - mPublicInterfaces[interfaceName] = sol::nil; - auto prevIt = mScriptOrder.rbegin(); - while (*prevIt != path) - prevIt++; - prevIt++; - while (prevIt != mScriptOrder.rend()) - { - sol::object& prevScript = mScripts[*(prevIt++)].mInterface; - sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); - if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) - { - mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); - break; - } - } - } - } - sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); - if (engineHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(engineHandlers)) - { - std::string_view handlerName = key.as(); - auto handlerIter = mEngineHandlers.find(handlerName); - if (handlerIter == mEngineHandlers.end()) - continue; - std::vector& list = handlerIter->second->mList; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } - sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); - if (eventHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(eventHandlers)) - { - EventHandlerList& list = mEventHandlers.find(key.as())->second; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } + return; // no such script + Script& script = scriptIter->second; + if (script.mInterface) + removeInterface(scriptId, script); + script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.erase(scriptIter); - mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); - return true; + for (auto& [_, handlers] : mEngineHandlers) + removeHandler(handlers->mList, scriptId); + for (auto& [_, handlers] : mEventHandlers) + removeHandler(handlers, scriptId); } - void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + void ScriptsContainer::insertInterface(int scriptId, const Script& script) { - for (auto& [key, value] : handlers) + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) { - std::string_view eventName = key.as(); - auto it = mEventHandlers.find(eventName); - if (it == mEventHandlers.end()) - it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; - it->second.push_back(value); - } - } - - void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) - { - for (auto& [key, value] : handlers) - { - std::string_view handlerName = key.as(); - if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) - continue; // save and load are handled separately - auto it = mEngineHandlers.find(handlerName); - if (it == mEngineHandlers.end()) - Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; else - it->second->mList.push_back(value); + { + next = &otherScript; + nextId = otherId; + break; + } } + if (prev && script.mOnOverride) + { + try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); } + catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); } + } + if (next && next->mOnOverride) + { + try { LuaUtil::call(*next->mOnOverride, *script.mInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + if (next == nullptr) + mPublicInterfaces[script.mInterfaceName] = *script.mInterface; + } + + void ScriptsContainer::removeInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) + { + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else + { + next = &otherScript; + nextId = otherId; + break; + } + } + if (next) + { + if (next->mOnOverride) + { + sol::object prevInterface = sol::nil; + if (prev) + prevInterface = *prev->mInterface; + try { LuaUtil::call(*next->mOnOverride, prevInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + } + else if (prev) + mPublicInterfaces[script.mInterfaceName] = *prev->mInterface; + else + mPublicInterfaces[script.mInterfaceName] = sol::nil; + } + + void ScriptsContainer::insertHandler(std::vector& list, int scriptId, sol::function fn) + { + list.emplace_back(); + int pos = list.size() - 1; + while (pos > 0 && list[pos - 1].mScriptId > scriptId) + { + list[pos] = std::move(list[pos - 1]); + pos--; + } + list[pos].mScriptId = scriptId; + list[pos].mFn = std::move(fn); + } + + void ScriptsContainer::removeHandler(std::vector& list, int scriptId) + { + list.erase(std::remove_if(list.begin(), list.end(), + [scriptId](const Handler& h){ return h.mScriptId == scriptId; }), + list.end()); } void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) @@ -191,13 +284,14 @@ namespace LuaUtil { try { - sol::object res = LuaUtil::call(list[i], data); + sol::object res = LuaUtil::call(list[i].mFn, data); if (res != sol::nil && !res.as()) break; // Skip other handlers if 'false' was returned. } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) + << "] eventHandler[" << eventName << "] failed. " << e.what(); } } } @@ -208,9 +302,19 @@ namespace LuaUtil mEngineHandlers[h->mName] = h; } + void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit) + { + try + { + const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData; + LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + } + catch (std::exception& e) { printError(scriptId, "onInit failed", e); } + } + void ScriptsContainer::save(ESM::LuaScripts& data) { - std::map> timers; + std::map> timers; auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) { if (!timer.mSerializable) @@ -220,78 +324,85 @@ namespace LuaUtil savedTimer.mUnit = timeUnit; savedTimer.mCallbackName = std::get(timer.mCallback); savedTimer.mCallbackArgument = timer.mSerializedArg; - if (timers.count(timer.mScript) == 0) - timers[timer.mScript] = {}; - timers[timer.mScript].push_back(std::move(savedTimer)); + timers[timer.mScriptId].push_back(std::move(savedTimer)); }; for (const Timer& timer : mSecondsTimersQueue) saveTimerFn(timer, TimeUnit::SECONDS); for (const Timer& timer : mHoursTimersQueue) saveTimerFn(timer, TimeUnit::HOURS); data.mScripts.clear(); - for (const std::string& path : mScriptOrder) + for (auto& [scriptId, script] : mScripts) { ESM::LuaScript savedScript; - savedScript.mScriptPath = path; - sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); - if (handler != sol::nil) + savedScript.mScriptPath = script.mHiddenData.get(ScriptId::KEY).mPath; + if (script.mOnSave) { try { - sol::object state = LuaUtil::call(handler); + sol::object state = LuaUtil::call(*script.mOnSave); savedScript.mData = serialize(state, mSerializer); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); - } + catch (std::exception& e) { printError(scriptId, "onSave failed", e); } } - auto timersIt = timers.find(path); + auto timersIt = timers.find(scriptId); if (timersIt != timers.end()) savedScript.mTimers = std::move(timersIt->second); data.mScripts.push_back(std::move(savedScript)); } } - void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + void ScriptsContainer::load(const ESM::LuaScripts& data) { - std::map scriptsWithoutSavedData; - if (resetScriptList) + removeAllScripts(); + const ScriptsConfiguration& cfg = mLua.getConfiguration(); + + std::map scripts; + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + scripts[scriptId] = nullptr; + for (const ESM::LuaScript& s : data.mScripts) { - removeAllScripts(); - for (const ESM::LuaScript& script : data.mScripts) - addNewScript(script.mScriptPath); - } - else - scriptsWithoutSavedData = mScripts; - mSecondsTimersQueue.clear(); - mHoursTimersQueue.clear(); - for (const ESM::LuaScript& script : data.mScripts) - { - auto iter = mScripts.find(script.mScriptPath); - if (iter == mScripts.end()) + std::optional scriptId = cfg.findId(s.mScriptPath); + if (!scriptId) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered"; continue; - scriptsWithoutSavedData.erase(iter->first); - iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - try + } + if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode))) { - sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler != sol::nil) + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here"; + continue; + } + scripts[*scriptId] = &s; + } + + for (const auto& [scriptId, savedScript] : scripts) + { + std::optional onInit, onLoad; + if (!addScript(scriptId, onInit, onLoad)) + continue; + if (savedScript == nullptr) + { + if (onInit) + callOnInit(scriptId, *onInit); + continue; + } + if (onLoad) + { + try { - sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); - LuaUtil::call(handler, state); + sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer); + sol::object initializationData = + deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer); + LuaUtil::call(*onLoad, state, initializationData); } + catch (std::exception& e) { printError(scriptId, "onLoad failed", e); } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); - } - for (const ESM::LuaTimer& savedTimer : script.mTimers) + for (const ESM::LuaTimer& savedTimer : savedScript->mTimers) { Timer timer; timer.mCallback = savedTimer.mCallbackName; timer.mSerializable = true; - timer.mScript = script.mScriptPath; + timer.mScriptId = scriptId; timer.mTime = savedTimer.mTime; try @@ -306,24 +417,10 @@ namespace LuaUtil else mSecondsTimersQueue.push_back(std::move(timer)); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); - } - } - } - for (auto& [path, script] : scriptsWithoutSavedData) - { - script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler == sol::nil) - continue; - try { LuaUtil::call(handler); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + catch (std::exception& e) { printError(scriptId, "can not load timer", e); } } } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } @@ -334,12 +431,13 @@ namespace LuaUtil script.mHiddenData[ScriptId::KEY] = sol::nil; } + // Note: shouldn't be called from destructor because mEngineHandlers has pointers on + // external objects that are already removed during child class destruction. void ScriptsContainer::removeAllScripts() { for (auto& [_, script] : mScripts) script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.clear(); - mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); mEventHandlers.clear(); @@ -351,17 +449,17 @@ namespace LuaUtil mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } - sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + sol::table ScriptsContainer::getHiddenData(int scriptId) { - auto it = mScripts.find(scriptPath); + auto it = mScripts.find(scriptId); if (it == mScripts.end()) throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); return it->second.mHiddenData; } - void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) { - getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + getHiddenData(scriptId)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); } void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) @@ -370,12 +468,12 @@ namespace LuaUtil std::push_heap(timerQueue.begin(), timerQueue.end()); } - void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg) { Timer t; t.mCallback = std::string(callbackName); - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = true; t.mTime = time; t.mArg = callbackArg; @@ -383,15 +481,15 @@ namespace LuaUtil insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); } - void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback) + void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback) { Timer t; - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = false; t.mTime = time; t.mCallback = mTemporaryCallbackCounter; - getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + getHiddenData(scriptId)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); mTemporaryCallbackCounter++; insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); @@ -401,7 +499,7 @@ namespace LuaUtil { try { - sol::table data = getHiddenData(t.mScript); + sol::table data = getHiddenData(t.mScriptId); if (t.mSerializable) { const std::string& callbackName = std::get(t.mCallback); @@ -421,10 +519,7 @@ namespace LuaUtil callbacks[id] = sol::nil; } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); - } + catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } } void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 69aa18e940..184072eef1 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -17,7 +17,7 @@ namespace LuaUtil // ScriptsContainer is a base class for all scripts containers (LocalScripts, // GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. // Scripts from different containers can interact to each other only via events. -// Scripts within one container can interact via interfaces (not implemented yet). +// Scripts within one container can interact via interfaces. // All scripts from one container have the same set of API packages available. // // Each script should return a table in a specific format that describes its @@ -42,11 +42,12 @@ namespace LuaUtil // -- An error is printed if unknown handler is specified. // engineHandlers = { // onUpdate = update, +// onInit = function(initData) ... end, -- used when the script is just created (not loaded) // onSave = function() return ... end, -// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave // -// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class -// -- and explicitly supports 'onSomethingElse' +// -- Works only if a child class has passed a EngineHandlerList +// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers. // onSomethingElse = function() print("something else") end // }, // @@ -65,30 +66,36 @@ namespace LuaUtil constexpr static std::string_view KEY = "_id"; ScriptsContainer* mContainer; - std::string mPath; + int mIndex; // index in LuaUtil::ScriptsConfiguration + std::string mPath; // path to the script source in VFS std::string toString() const; }; using TimeUnit = ESM::LuaTimer::TimeUnit; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. - ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); + // `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is + // stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode). + ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0); + ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; virtual ~ScriptsContainer(); + ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; } + // Adds package that will be available (via `require`) for all scripts in the container. // Automatically applies LuaUtil::makeReadOnly to the package. - void addPackage(const std::string& packageName, sol::object package); + void addPackage(std::string packageName, sol::object package); - // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. - // Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`. - // `false` can be returned if either file not found or has syntax errors or such script already exists in the container. - bool addNewScript(const std::string& path); + // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a new script, + // adds it to the container, and calls onInit for this script. Returns `true` if the script was successfully added. + // The script should have CUSTOM flag. If the flag is not set, or file not found, or has syntax errors, returns false. + // If such script already exists in the container, then also returns false. + bool addCustomScript(int scriptId); - // Removes script. Returns `true` if it was successfully removed. - bool removeScript(const std::string& path); - void removeAllScripts(); + bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } + void removeScript(int scriptId); // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. void processTimers(double gameSeconds, double gameHours); @@ -107,22 +114,22 @@ namespace LuaUtil // only built-in types and types from util package can be serialized. void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used. + void addAutoStartedScripts(); + + // Removes all scripts including the auto started. + void removeAllScripts(); + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. void save(ESM::LuaScripts&); - // Calls engineHandler "onLoad" for every script with given data. - // If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts. - // If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called. - void load(const ESM::LuaScripts&, bool resetScriptList); - - // Returns the hidden data of a script. - // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, - // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. - sol::table getHiddenData(const std::string& scriptPath); + // Removes all scripts; starts scripts according to `autoStartMode` and + // loads the savedScripts. Runs "onLoad" for each script. + void load(const ESM::LuaScripts& savedScripts); // Callbacks for serializable timers should be registered in advance. // The script with the given path should already present in the container. - void registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback); + void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); // Sets up a timer, that can be automatically saved and loaded. // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). @@ -130,18 +137,24 @@ namespace LuaUtil // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. // callbackName - callback (should be registered in advance) for this timer. // callbackArg - parameter for the callback (should be serializable). - void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg); // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. - void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback); + void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback); protected: + struct Handler + { + int mScriptId; + sol::function mFn; + }; + struct EngineHandlerList { std::string_view mName; - std::vector mList; + std::vector mList; // "name" must be string literal explicit EngineHandlerList(std::string_view name) : mName(name) {} @@ -151,12 +164,13 @@ namespace LuaUtil template void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) { - for (sol::protected_function& handler : handlers.mList) + for (Handler& handler : handlers.mList) { - try { LuaUtil::call(handler, args...); } + try { LuaUtil::call(handler.mFn, args...); } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " + << handlers.mName << " failed. " << e.what(); } } } @@ -171,34 +185,49 @@ namespace LuaUtil private: struct Script { - sol::object mInterface; // returned value of the script (sol::table or nil) + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; + std::string mInterfaceName; sol::table mHiddenData; }; struct Timer { double mTime; bool mSerializable; - std::string mScript; + int mScriptId; std::variant mCallback; // string if serializable, integer otherwise sol::object mArg; std::string mSerializedArg; bool operator<(const Timer& t) const { return mTime > t.mTime; } }; - using EventHandlerList = std::vector; + using EventHandlerList = std::vector; - void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); - void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + // Add to container without calling onInit/onLoad. + bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); + // Returns the hidden data of a script. + // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, + // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. + sol::table getHiddenData(int scriptId); + + void printError(int scriptId, std::string_view msg, const std::exception& e); + const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } + void callOnInit(int scriptId, const sol::function& onInit); void callTimer(const Timer& t); void updateTimerQueue(std::vector& timerQueue, double time); static void insertTimer(std::vector& timerQueue, Timer&& t); + static void insertHandler(std::vector& list, int scriptId, sol::function fn); + static void removeHandler(std::vector& list, int scriptId); + void insertInterface(int scriptId, const Script& script); + void removeInterface(int scriptId, const Script& script); + ESM::LuaScriptCfg::Flags mAutoStartMode; const UserdataSerializer* mSerializer = nullptr; - std::map API; + std::map mAPI; - std::vector mScriptOrder; - std::map mScripts; + std::map mScripts; sol::table mPublicInterfaces; EngineHandlerList mUpdateHandlers{"onUpdate"}; From 4ec7f0625e08dad96ff016894d865ca8050098a0 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 10 Oct 2021 22:44:50 +0200 Subject: [PATCH 030/110] Store Lua timers in std::map rather than in sol::table. --- components/lua/scriptscontainer.cpp | 31 ++++++++++------------------- components/lua/scriptscontainer.hpp | 8 ++++---- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index f4922a8b6b..c92116f1d9 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -15,9 +15,6 @@ namespace LuaUtil static constexpr std::string_view HANDLER_LOAD = "onLoad"; static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; - static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; - static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; - std::string ScriptsContainer::ScriptId::toString() const { std::string res = mContainer->mNamePrefix; @@ -78,8 +75,6 @@ namespace LuaUtil Script& script = mScripts[scriptId]; script.mHiddenData = mLua.newTable(); script.mHiddenData[ScriptId::KEY] = ScriptId{this, scriptId, path}; - script.mHiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); - script.mHiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); if (scriptOutput == sol::nil) return true; @@ -449,17 +444,17 @@ namespace LuaUtil mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } - sol::table ScriptsContainer::getHiddenData(int scriptId) + ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId) { auto it = mScripts.find(scriptId); if (it == mScripts.end()) - throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); - return it->second.mHiddenData; + throw std::logic_error("Script doesn't exist"); + return it->second; } void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) { - getHiddenData(scriptId)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + getScript(scriptId).mRegisteredCallbacks.emplace(std::string(callbackName), std::move(callback)); } void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) @@ -489,7 +484,7 @@ namespace LuaUtil t.mTime = time; t.mCallback = mTemporaryCallbackCounter; - getHiddenData(scriptId)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback)); mTemporaryCallbackCounter++; insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); @@ -499,24 +494,20 @@ namespace LuaUtil { try { - sol::table data = getHiddenData(t.mScriptId); + Script& script = getScript(t.mScriptId); if (t.mSerializable) { const std::string& callbackName = std::get(t.mCallback); - sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName]; - if (!callback.is()) + auto it = script.mRegisteredCallbacks.find(callbackName); + if (it == script.mRegisteredCallbacks.end()) throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); - LuaUtil::call(callback, t.mArg); + LuaUtil::call(it->second, t.mArg); } else { int64_t id = std::get(t.mCallback); - sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS]; - sol::object callback = callbacks[id]; - if (!callback.is()) - throw std::logic_error("Temporary timer callback doesn't exist"); - LuaUtil::call(callback); - callbacks[id] = sol::nil; + LuaUtil::call(script.mTemporaryCallbacks.at(id)); + script.mTemporaryCallbacks.erase(id); } } catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 184072eef1..b25c69b5b4 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -190,6 +190,8 @@ namespace LuaUtil std::optional mInterface; std::string mInterfaceName; sol::table mHiddenData; + std::map mRegisteredCallbacks; + std::map mTemporaryCallbacks; }; struct Timer { @@ -207,10 +209,8 @@ namespace LuaUtil // Add to container without calling onInit/onLoad. bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); - // Returns the hidden data of a script. - // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, - // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. - sol::table getHiddenData(int scriptId); + // Returns script by id (throws an exception if doesn't exist) + Script& getScript(int scriptId); void printError(int scriptId, std::string_view msg, const std::exception& e); const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } From 47c89567fbfe95ebba2f2c501d29baa37be71772 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 26 Sep 2021 15:21:46 +0200 Subject: [PATCH 031/110] Load LuaScriptsCfg from both *.omwscripts and *.omwaddon files. --- apps/openmw/engine.cpp | 7 +--- apps/openmw/engine.hpp | 2 - apps/openmw/main.cpp | 8 ++-- apps/openmw/mwlua/luamanagerimp.cpp | 22 ++-------- apps/openmw/mwlua/luamanagerimp.hpp | 5 +-- apps/openmw/mwworld/esmstore.cpp | 42 ++++++++++++++++++- apps/openmw/mwworld/esmstore.hpp | 10 +++++ apps/openmw/mwworld/worldimp.cpp | 14 +++++++ apps/openmw/options.cpp | 5 +-- apps/openmw_test_suite/mwworld/test_store.cpp | 6 ++- components/esm/defs.hpp | 1 + 11 files changed, 81 insertions(+), 41 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index bbe4f25f69..bc31243ffc 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -495,11 +495,6 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) mGroundcoverFiles.emplace_back(file); } -void OMW::Engine::addLuaScriptListFile(const std::string& file) -{ - mLuaScriptListFiles.push_back(file); -} - void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; @@ -714,7 +709,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mViewer->addEventHandler(mScreenCaptureHandler); - mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles); + mLuaManager = new MWLua::LuaManager(mVFS.get()); mEnvironment.setLuaManager(mLuaManager); // Create input and UI first to set up a bootstrapping environment for diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 290fd890a6..5fe032d27e 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -72,7 +72,6 @@ namespace OMW std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; - std::vector mLuaScriptListFiles; bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -146,7 +145,6 @@ namespace OMW */ void addContentFile(const std::string& file); void addGroundcoverFile(const std::string& file); - void addLuaScriptListFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d1f9fd1787..1cf7abe2f2 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -124,9 +124,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat engine.addGroundcoverFile(file); } - StringsVector luaScriptLists = variables["lua-scripts"].as().toStdStringVector(); - for (const auto& file : luaScriptLists) - engine.addLuaScriptListFile(file); + if (variables.count("lua-scripts")) + { + Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. " + "Please update them to a version which uses the new omwscripts format."; + } // startup-settings engine.setCell(variables["start"].as().toStdString()); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 7a4b319f27..30866faf56 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -11,6 +11,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "luabindings.hpp" @@ -19,8 +20,7 @@ namespace MWLua { - LuaManager::LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles) : - mVFS(vfs), mOMWScriptsFiles(std::move(OMWScriptsFiles)), mLua(vfs, &mConfiguration) + LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); @@ -34,23 +34,7 @@ namespace MWLua void LuaManager::initConfiguration() { - ESM::LuaScriptsCfg cfg; - for (const std::string& file : mOMWScriptsFiles) - { - if (!Misc::StringUtils::endsWith(file, ".omwscripts")) - { - Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << file << "'"; - continue; - } - try - { - std::string content(std::istreambuf_iterator(*mVFS->get(file)), {}); - LuaUtil::parseOMWScripts(cfg, content); - } - catch (std::exception& e) { Log(Debug::Error) << e.what(); } - } - // TODO: Add data from content files - mConfiguration.init(cfg); + mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().getLuaScriptsCfg()); Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; for (size_t i = 0; i < mConfiguration.size(); ++i) Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index be80cfe2e7..b9151de685 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -35,7 +35,7 @@ namespace MWLua class LuaManager : public MWBase::LuaManager { public: - LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles); + LuaManager(const VFS::Manager* vfs); // Called by engine.cpp when the environment is fully initialized. void init(); @@ -97,9 +97,6 @@ namespace MWLua void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); - const VFS::Manager* mVFS; - const std::vector mOMWScriptsFiles; - bool mInitialized = false; bool mGlobalScriptsStarted = false; LuaUtil::ScriptsConfiguration mConfiguration; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index c5b0fff00f..66aba9139e 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -1,14 +1,16 @@ #include "esmstore.hpp" #include +#include #include #include #include -#include #include #include +#include +#include #include #include "../mwmechanics/spelllist.hpp" @@ -166,7 +168,10 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) std::string fname = mast.name; int index = ~0; for (int i = 0; i < esm.getIndex(); i++) { - const std::string candidate = allPlugins->at(i).getContext().filename; + ESM::ESMReader& reader = allPlugins->at(i); + if (reader.getFileSize() == 0) + continue; // Content file in non-ESM format + const std::string candidate = reader.getContext().filename; std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { index = i; @@ -213,6 +218,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) // ignore project file only records esm.skipRecord(); } + else if (n.toInt() == ESM::REC_LUAL) + { + ESM::LuaScriptsCfg cfg; + cfg.load(esm); + // TODO: update refnums in cfg.mScripts[].mInitializationData according to load order + mLuaContent.push_back(std::move(cfg)); + } else { throw std::runtime_error("Unknown record: " + n.toString()); } @@ -234,6 +246,32 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } } +ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const +{ + ESM::LuaScriptsCfg cfg; + for (const LuaContent& c : mLuaContent) + { + if (std::holds_alternative(c)) + { + // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. + // It is important for the `reloadlua` console command. + try + { + auto file = std::ifstream(std::get(c)); + std::string fileContent(std::istreambuf_iterator(file), {}); + LuaUtil::parseOMWScripts(cfg, fileContent); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + else + { + const ESM::LuaScriptsCfg& addition = std::get(c); + cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); + } + } + return cfg; +} + void ESMStore::setUp(bool validateRecords) { mIds.clear(); diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6ad479f8bf..22b58f77dd 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "store.hpp" @@ -92,7 +93,16 @@ namespace MWWorld template void removeMissingObjects(Store& store); + + using LuaContent = std::variant< + ESM::LuaScriptsCfg, // data from an omwaddon + std::string>; // path to an omwscripts file + std::vector mLuaContent; + public: + void addOMWScripts(std::string filePath) { mLuaContent.push_back(std::move(filePath)); } + ESM::LuaScriptsCfg getLuaScriptsCfg() const; + /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ccd78170bb..21e2d8380a 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -111,6 +111,17 @@ namespace MWWorld LoadersContainer mLoaders; }; + struct OMWScriptsLoader : public ContentLoader + { + ESMStore& mStore; + OMWScriptsLoader(Loading::Listener& listener, ESMStore& store) : ContentLoader(listener), mStore(store) {} + void load(const boost::filesystem::path& filepath, int& index) override + { + ContentLoader::load(filepath.filename(), index); + mStore.addOMWScripts(filepath.string()); + } + }; + void World::adjustSky() { if (mSky && (isCellExterior() || isCellQuasiExterior())) @@ -156,6 +167,9 @@ namespace MWWorld gameContentLoader.addLoader(".omwaddon", &esmLoader); gameContentLoader.addLoader(".project", &esmLoader); + OMWScriptsLoader omwScriptsLoader(*listener, mStore); + gameContentLoader.addLoader(".omwscripts", &omwScriptsLoader); + loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader); listener->loadingOff(); diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index 62ea0910dd..d68809468c 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -40,14 +40,11 @@ namespace OpenMW "set initial cell") ("content", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") ("groundcover", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") - ("lua-scripts", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") - ("no-sound", bpo::value()->implicit_value(true) ->default_value(false), "disable all sounds") diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index f3b2bb3dcb..3fe479587c 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwmechanics/spelllist.hpp" @@ -88,7 +89,10 @@ struct ContentFileTest : public ::testing::Test std::vector contentFiles = variables["content"].as().toStdStringVector(); for (auto & contentFile : contentFiles) - mContentFiles.push_back(collections.getPath(contentFile)); + { + if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts")) + mContentFiles.push_back(collections.getPath(contentFile)); + } } protected: diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 7f2fe19cc5..254e66ec3a 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -165,6 +165,7 @@ enum RecNameInts // format 1 REC_FILT = FourCC<'F','I','L','T'>::value, REC_DBGP = FourCC<'D','B','G','P'>::value, ///< only used in project files + REC_LUAL = FourCC<'L','U','A','L'>::value, // LuaScriptsCfg // format 16 - Lua scripts in saved games REC_LUAM = FourCC<'L','U','A','M'>::value, // LuaManager data From 19a0fde278b35609cff2ddf8c6a990f1cde062a2 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 17 Oct 2021 10:29:10 +0200 Subject: [PATCH 032/110] Filter saves by the first esm/omwgame rather than by the first content file (that can be a universal omwaddon/omwscripts) --- apps/openmw/engine.cpp | 2 +- apps/openmw/mwstate/character.cpp | 12 ++++++++++-- apps/openmw/mwstate/character.hpp | 2 ++ apps/openmw/mwstate/charactermanager.cpp | 4 ++-- apps/openmw/mwstate/charactermanager.hpp | 2 +- apps/openmw/mwstate/statemanagerimp.cpp | 4 ++-- apps/openmw/mwstate/statemanagerimp.hpp | 2 +- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index bc31243ffc..58dabd2cee 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -669,7 +669,7 @@ void OMW::Engine::setWindowIcon() void OMW::Engine::prepareEngine (Settings::Manager & settings) { mEnvironment.setStateManager ( - new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles)); createWindow(settings); diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index df1ab1bdfb..59ddd2cd10 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -13,6 +13,15 @@ bool MWState::operator< (const Slot& left, const Slot& right) return left.mTimeStamp& contentFiles) +{ + for (const std::string& c : contentFiles) + { + if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) + return c; + } + return ""; +} void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game) { @@ -30,8 +39,7 @@ void MWState::Character::addSlot (const boost::filesystem::path& path, const std slot.mProfile.load (reader); - if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= - Misc::StringUtils::lowerCase (game)) + if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore mSlots.push_back (slot); diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 32c79a183e..e12de9ca64 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -16,6 +16,8 @@ namespace MWState bool operator< (const Slot& left, const Slot& right); + std::string getFirstGameFile(const std::vector& contentFiles); + class Character { public: diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index a324dfe0f7..027a4f38a4 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -6,8 +6,8 @@ #include MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, - const std::string& game) -: mPath (saves), mCurrent (nullptr), mGame (game) + const std::vector& contentFiles) +: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles)) { if (!boost::filesystem::is_directory (mPath)) { diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 2daf73401f..8b3f2b8f8f 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -29,7 +29,7 @@ namespace MWState public: - CharacterManager (const boost::filesystem::path& saves, const std::string& game); + CharacterManager (const boost::filesystem::path& saves, const std::vector& contentFiles); Character *getCurrentCharacter (); ///< @note May return null diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 5e51e2f621..b9825a0f90 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -88,8 +88,8 @@ std::map MWState::StateManager::buildContentFileIndexMap (const ESM::E return map; } -MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) -: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) +MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles) +: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, contentFiles), mTimePlayed (0) { } diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 3534dabf2b..a29e72b3ad 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -31,7 +31,7 @@ namespace MWState public: - StateManager (const boost::filesystem::path& saves, const std::string& game); + StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles); void requestQuit() override; From 95faeaa9c1e45a62c5ab4a35283224dcd9ae7338 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 17 Oct 2021 19:10:09 +0200 Subject: [PATCH 033/110] Fix: after reloadlua call onActive for all active scripts --- apps/openmw/mwlua/luamanagerimp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 30866faf56..5695883691 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -432,6 +432,8 @@ namespace MWLua scripts->save(data); scripts->load(data); } + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->receiveEngineEvent(LocalScripts::OnActive()); } } From dd96eba2b01a3430413532a63c2f9db9c675e109 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 10 Oct 2021 22:43:47 +0200 Subject: [PATCH 034/110] Update OpenMW Lua docs --- .../lua-scripting/engine_handlers.rst | 14 +- .../reference/lua-scripting/overview.rst | 126 +++++++++++------- docs/source/reference/modding/extended.rst | 14 +- files/lua_api/openmw/core.lua | 20 ++- 4 files changed, 113 insertions(+), 61 deletions(-) diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index bcbee4349e..ef74684182 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -6,14 +6,22 @@ Engine handler is a function defined by a script, that can be called by the engi +---------------------------------------------------------------------------------------------------------+ | **Can be defined by any script** | +----------------------------------+----------------------------------------------------------------------+ +| onInit(initData) | | Called once when the script is created (not loaded). `InitData can`| +| | | `be assigned to a script in openmw-cs (not yet implemented)`. | +| | | ``onInterfaceOverride`` can be called before ``onInit``. | ++----------------------------------+----------------------------------------------------------------------+ | onUpdate(dt) | | Called every frame if game not paused. `dt` is the time | | | | from the last update in seconds. | +----------------------------------+----------------------------------------------------------------------+ -| onSave() -> data | | Called when the game is saving. May be called in inactive | +| onSave() -> savedData | | Called when the game is saving. May be called in inactive | | | | state, so it shouldn't use `openmw.nearby`. | +----------------------------------+----------------------------------------------------------------------+ -| onLoad(data) | | Called on loading with the data previosly returned by | -| | | onSave. During loading the object is always inactive. | +| onLoad(savedData, initData) | | Called on loading with the data previosly returned by | +| | | onSave. During loading the object is always inactive. initData is | +| | | the same as in onInit. | ++----------------------------------+----------------------------------------------------------------------+ +| onInterfaceOverride(base) | | Called if the current script has an interface and overrides an | +| | | interface (``base``) of another script. | +----------------------------------+----------------------------------------------------------------------+ | **Only for global scripts** | +----------------------------------+----------------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 44c79c35d8..c32fc74fa5 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -73,7 +73,7 @@ Let's write a simple example of a `Player script`: .. code-block:: Lua - -- Saved to my_lua_mod/example/player.lua + -- Save to my_lua_mod/example/player.lua local ui = require('openmw.ui') @@ -87,42 +87,82 @@ Let's write a simple example of a `Player script`: } } -In order to attach it to the player we also need a global script: +The script will be used only if it is specified in one of content files. +OpenMW Lua is an inclusive OpenMW feature, so it can not be controlled by ESP/ESM. +The options are: -.. code-block:: Lua - - -- Saved to my_lua_mod/example/global.lua - - return { - engineHandlers = { - onPlayerAdded = function(player) player:addScript('example/player.lua') end - } - } - -And one more file -- to start the global script: +1. Create text file "my_lua_mod.omwscripts" with the following line: :: - # Saved to my_lua_mod/my_lua_mod.omwscripts + PLAYER: example/player.lua - # It is just a list of global scripts to run. Each file is on a separate line. - example/global.lua +2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon". -Finally :ref:`register ` it in ``openmw.cfg``: + +Enable it in ``openmw.cfg`` the same way as any other mod: :: data=path/to/my_lua_mod - lua-scripts=my_lua_mod.omwscripts + content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon Now every time the player presses "X" on a keyboard, a message is shown. + +Format of ``.omwscripts`` +========================= + +:: + + # Lines starting with '#' are comments + + GLOBAL: my_mod/some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER: my_mod/player.lua + + # Local script that will be automatically attached to every NPC and every creature in the game + NPC, CREATURE: my_mod/some_other_script.lua + + # Local script that can be attached to any object by a global script + CUSTOM: my_mod/something.lua + + # Local script that will be automatically attached to any Container AND can be + # attached to any other object by a global script. + CONTAINER, CUSTOM: my_mod/container.lua + +Each script is described by one line: +``: ``. +The order of lines determines the script load order (i.e. script priorities). + +Possible flags are: + +- ``GLOBAL`` - a global script; always active, can not by stopped; +- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script; +- ``PLAYER`` - an auto started player script; +- ``ACTIVATOR`` - a local script that will be automatically attached to any activator; +- ``ARMOR`` - a local script that will be automatically attached to any armor; +- ``BOOK`` - a local script that will be automatically attached to any book; +- ``CLOTHING`` - a local script that will be automatically attached to any clothing; +- ``CONTAINER`` - a local script that will be automatically attached to any container; +- ``CREATURE`` - a local script that will be automatically attached to any creature; +- ``DOOR`` - a local script that will be automatically attached to any door; +- ``INGREDIENT`` - a local script that will be automatically attached to any ingredient; +- ``LIGHT`` - a local script that will be automatically attached to any light; +- ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item; +- ``NPC`` - a local script that will be automatically attached to any NPC; +- ``POTION`` - a local script that will be automatically attached to any potion; +- ``WEAPON`` - a local script that will be automatically attached to any weapon. + +Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator. + Hot reloading ============= It is possible to modify a script without restarting OpenMW. To apply changes, open the in-game console and run the command: ``reloadlua``. This will restart all Lua scripts using the `onSave and onLoad`_ handlers the same way as if the game was saved or loaded. -It works only with existing ``*.lua`` files that are not packed to any archives. Adding new scripts or modifying ``*.omwscripts`` files always requires restarting the game. +It reloads all ``.omwscripts`` files and ``.lua`` files that are not packed to any archives. ``.omwaddon`` files and scripts packed to BSA can not be changed without restarting the game. Script structure ================ @@ -196,7 +236,7 @@ Engine handlers An engine handler is a function defined by a script, that can be called by the engine. I.e. it is an engine-to-script interaction. Not visible to other scripts. If several scripts register an engine handler with the same name, -the engine calls all of them in the same order as the scripts were started. +the engine calls all of them according to the load order (i.e. the order of ``content=`` entries in ``openmw.cfg``) and the order of scripts in ``omwaddon/omwscripts``. Some engine handlers are allowed only for global, or only for local/player scripts. Some are universal. See :ref:`Engine handlers reference`. @@ -210,12 +250,6 @@ The value that `onSave` returns will be passed to `onLoad` when the game is load It is the only way to save the internal state of a script. All other script variables will be lost after closing the game. The saved state must be :ref:`serializable `. -The list of active global scripts is controlled by ``*.omwscripts`` files. Loading a save doesn't synchronize -the list of global scripts with those that were active previously, it only calls `onLoad` for those currently active. - -For local scripts the situation is different. When a save is loading, it tries to run all local scripts that were saved. -So if ``lua-scripts=`` entries of some mod are removed, but ``data=`` entries are still enabled, then local scripts from the mod may still run. - `onSave` and `onLoad` can be called even for objects in inactive state, so it shouldn't use `openmw.nearby`. An example: @@ -366,26 +400,28 @@ Overriding the interface and adding a debug output: .. code-block:: Lua - local interfaces = require('openmw.interfaces') + local baseInterface = nil -- will be assigned by `onInterfaceOverride` + interface = { + version = 1, + doSomething = function(x, y) + print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) + baseInterface.doSomething(x, y) -- calls the original `doSomething` - -- it is important to save it before returning the new interface - local orig = interfaces.SomeUtils - - return { - interfaceName = "SomeUtils" - interface = { - version = orig.version, - doSomething = function(x, y) - print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) - orig.doSomething(x, y) -- calls the original `doSomething` - - -- WRONG! Would lead to an infinite recursion. - -- interfaces.SomeUtils.doSomething(x, y) - end, - } + -- WRONG! Would lead to an infinite recursion. + -- local interfaces = require('openmw.interfaces') + -- interfaces.SomeUtils.doSomething(x, y) + end, } -A general recomendation about overriding is that the new interface should be fully compatible with the old one. + return { + interfaceName = "SomeUtils", + interface = interface, + engineHandlers = { + onInterfaceOverride = function(base) baseInterface = base end, + }, + } + +A general recommendation about overriding is that the new interface should be fully compatible with the old one. So it is fine to change the behaviour of `SomeUtils.doSomething`, but if you want to add a completely new function, it would be better to create a new interface for it. For example `SomeUtilsExtended` with an additional function `doSomethingElse`. @@ -418,7 +454,7 @@ Events are the main way of interacting between local and global scripts. They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient. If several scripts register handlers for the same event, the handlers will be called in reverse order (opposite to engine handlers). -I.e. the handler from the last attached script will be called first. +I.e. the handler from the last script in the load order will be called first. Return value 'false' means "skip all other handlers for this event". Any other return value (including nil) means nothing. @@ -471,7 +507,7 @@ The protection mod attaches an additional local script to every actor. The scrip eventHandlers = { DamagedByDarkPower = reduceDarkDamage }, } -In order to be able to intercept the event, the protection script should be attached after the original script (i.e. below in the load order). +In order to be able to intercept the event, the protection script should be placed in the load order below the original script. Timers diff --git a/docs/source/reference/modding/extended.rst b/docs/source/reference/modding/extended.rst index f107617b33..db7df2e916 100644 --- a/docs/source/reference/modding/extended.rst +++ b/docs/source/reference/modding/extended.rst @@ -333,17 +333,9 @@ Lua scripting OpenMW supports Lua scripts. See :ref:`Lua scripting documentation `. It is not compatible with MWSE. A mod with Lua scripts will work only if it was developed specifically for OpenMW. -Mods can contain ``*.omwscripts`` files. They should be registered in the ``openmw.cfg`` via "lua-scripts" entries. The order of the "lua-scripts" entries can be important. If "some_lua_mod" uses API provided by "another_lua_mod", then omwscripts from "another_lua_mod" should be registered first. For example: - -:: - - data="path/to/another_lua_mod" - content=another_lua_mod.omwaddon - lua-scripts=another_lua_mod.omwscripts - - data="path/to/some_lua_mod" - content=some_lua_mod.omwaddon - lua-scripts=some_lua_mod.omwscripts +Installation of a Lua mod is the same as of any other mod: add ``data=`` and ``content=`` entries to ``openmw.cfg``. +Files with suffix ``.omwscripts`` are special type of content files and should also be enabled using ``content=`` entries. +Note that for some mods load order can be important. .. _`Graphic Herbalism`: https://www.nexusmods.com/morrowind/mods/46599 .. _`OpenMW Containers Animated`: https://www.nexusmods.com/morrowind/mods/46232 diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 50706b9770..35513bbb79 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -192,10 +192,26 @@ ------------------------------------------------------------------------------- -- Add new local script to the object. --- Can be called only from a global script. +-- Can be called only from a global script. Script should be specified in a content +-- file (omwgame/omwaddon/omwscripts) with a CUSTOM flag. -- @function [parent=#GameObject] addScript -- @param self --- @param #string scriptPath Path to the script in OpenMW virtual filesystem +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. + +------------------------------------------------------------------------------- +-- Whether a script with given path is attached to this object. +-- Can be called only from a global script. +-- @function [parent=#GameObject] hasScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. +-- @return #boolean + +------------------------------------------------------------------------------- +-- Removes script that was attached by `addScript` +-- Can be called only from a global script. +-- @function [parent=#GameObject] removeScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. ------------------------------------------------------------------------------- -- Moves object to given cell and position. From 3ee6657768b6af74489ac6c16d2eecceb64cd412 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 26 Oct 2021 22:23:24 +0400 Subject: [PATCH 035/110] Early out from LOS check when source and target is the same (bug 5913) --- CHANGELOG.md | 1 + apps/openmw/mwphysics/physicssystem.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d609b0ae7f..de0d4e17d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention Bug #5842: GetDisposition adds temporary disposition change from different actors Bug #5863: GetEffect should return true after the player has teleported + Bug #5913: Failed assertion during Ritual of Trees quest Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 2a5bcb58bd..f7678bed27 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -370,6 +370,8 @@ namespace MWPhysics bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { + if (actor1 == actor2) return true; + const auto it1 = mActors.find(actor1.mRef); const auto it2 = mActors.find(actor2.mRef); if (it1 == mActors.end() || it2 == mActors.end()) From cd358ce1f999c267e61eb57533379bcf8611ffa8 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 27 Oct 2021 11:48:17 +0200 Subject: [PATCH 036/110] Update CMakeLists.txt --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 537c90670d..13196a4aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,8 +707,7 @@ endif() if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") - set_target_properties(components PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") - set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") + set_target_properties(components openmw PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") endif() # Apple bundling From e436147c10f5ef164d42d4cf3ade8548f0f2e1bd Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Wed, 27 Oct 2021 09:16:01 -0700 Subject: [PATCH 037/110] improved toggle world --- apps/openmw/mwrender/renderingmanager.cpp | 8 +-- apps/openmw/mwrender/vismask.hpp | 3 ++ apps/openmw/mwrender/water.cpp | 62 +++++++++++++++++++---- apps/openmw/mwrender/water.hpp | 3 ++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 1f29c45182..7b8393f690 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -753,12 +753,12 @@ namespace MWRender else if (mode == Render_Scene) { unsigned int mask = mViewer->getCamera()->getCullMask(); - bool enabled = mask&Mask_Scene; - enabled = !enabled; + bool enabled = !(mask&sToggleWorldMask); if (enabled) - mask |= Mask_Scene; + mask |= sToggleWorldMask; else - mask &= ~Mask_Scene; + mask &= ~sToggleWorldMask; + mWater->showWorld(enabled); mViewer->getCamera()->setCullMask(mask); return enabled; } diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 87ca9415fa..a7a28614cb 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -58,6 +58,9 @@ namespace MWRender Mask_Groundcover = (1<<20), }; + // Defines masks to remove when using ToggleWorld command + constexpr static unsigned int sToggleWorldMask = Mask_Debug | Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; + } #endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 37368b8e7a..d8f92d1d1f 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -260,6 +260,7 @@ class Refraction : public SceneUtil::RTTNode public: Refraction(uint32_t rttSize) : RTTNode(rttSize, rttSize, 1, false) + , mNodeMask(Refraction::sDefaultCullMask) { mClipCullNode = new ClipCullNode; } @@ -273,8 +274,6 @@ public: camera->addCullCallback(new InheritViewPointCallback); camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - camera->setCullMask(Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover); - // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) @@ -293,6 +292,7 @@ public: void apply(osg::Camera* camera) override { camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setScene(osg::Node* scene) @@ -314,10 +314,22 @@ public: mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } + void showWorld(bool show) + { + if (show) + mNodeMask = Refraction::sDefaultCullMask; + else + mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; + } + private: osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + + unsigned int mNodeMask; + + static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover; }; class Reflection : public SceneUtil::RTTNode @@ -357,15 +369,8 @@ public: void setInterior(bool isInterior) { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail)); - unsigned int extraMask = 0; - if(reflectionDetail >= 1) extraMask |= Mask_Terrain; - if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; - if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - mNodeMask = Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + mInterior = isInterior; + mNodeMask = calcNodeMask(); } void setWaterLevel(float waterLevel) @@ -382,11 +387,34 @@ public: mClipCullNode->addChild(scene); } + void showWorld(bool show) + { + if (show) + mNodeMask = calcNodeMask(); + else + mNodeMask = calcNodeMask() & ~sToggleWorldMask; + } + private: + + unsigned int calcNodeMask() + { + int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); + reflectionDetail = std::min(5, std::max(mInterior ? 2 : 0, reflectionDetail)); + unsigned int extraMask = 0; + if(reflectionDetail >= 1) extraMask |= Mask_Terrain; + if(reflectionDetail >= 2) extraMask |= Mask_Static; + if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; + if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; + return Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + } + osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Node::NodeMask mNodeMask; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + bool mInterior; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -422,6 +450,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mToggled(true) , mTop(0) , mInterior(false) + , mShowWorld(true) , mCullCallback(nullptr) , mShaderWaterStateSetUpdater(nullptr) { @@ -519,6 +548,8 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } + showWorld(mShowWorld); + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else @@ -812,4 +843,13 @@ void Water::clearRipples() mSimulation->clear(); } +void Water::showWorld(bool show) +{ + if (mReflection) + mReflection->showWorld(show); + if (mRefraction) + mRefraction->showWorld(show); + mShowWorld = show; +} + } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 719e4fdc2b..c7acbf708f 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -71,6 +71,7 @@ namespace MWRender bool mToggled; float mTop; bool mInterior; + bool mShowWorld; osg::Callback* mCullCallback; osg::ref_ptr mShaderWaterStateSetUpdater; @@ -124,6 +125,8 @@ namespace MWRender osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); + + void showWorld(bool show); }; } From 5009b66ef5bccc447603cc0d58592106cf0cd1a0 Mon Sep 17 00:00:00 2001 From: Frederic Chardon Date: Thu, 7 Oct 2021 21:47:05 +0200 Subject: [PATCH 038/110] Use std::variant in the physics simulation for the different types of objects. For now only support only for actors. --- apps/openmw/mwphysics/actor.cpp | 12 ++ apps/openmw/mwphysics/actor.hpp | 3 + apps/openmw/mwphysics/movementsolver.cpp | 2 +- apps/openmw/mwphysics/movementsolver.hpp | 2 +- apps/openmw/mwphysics/mtphysics.cpp | 175 ++++++++++++++++------- apps/openmw/mwphysics/mtphysics.hpp | 8 +- apps/openmw/mwphysics/physicssystem.cpp | 34 ++--- apps/openmw/mwphysics/physicssystem.hpp | 6 +- 8 files changed, 155 insertions(+), 87 deletions(-) diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e140141e32..c7e2308e2f 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -12,6 +12,7 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include "trace.h" #include @@ -303,4 +304,15 @@ osg::Vec3f Actor::velocity() return std::exchange(mVelocity, osg::Vec3f()); } +bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const +{ + const float halfZ = getHalfExtents().z(); + const osg::Vec3f actorPosition = getPosition(); + const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); + const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); + MWPhysics::ActorTracer tracer; + tracer.doTrace(getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); +} + } diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 0846401c1d..d2ebd78379 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -12,6 +12,7 @@ class btCollisionShape; class btCollisionObject; +class btCollisionWorld; class btConvexShape; namespace Resource @@ -165,6 +166,8 @@ namespace MWPhysics void setVelocity(osg::Vec3f velocity); osg::Vec3f velocity(); + bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; + private: MWWorld::Ptr mStandingOnPtr; /// Removes then re-adds the collision object to the dynamics world diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 44a5391f0d..29810e9085 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -116,7 +116,7 @@ namespace MWPhysics } void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, - WorldFrameData& worldData) + const WorldFrameData& worldData) { // Reset per-frame data actor.mWalkingOnWater = false; diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 30733eeec8..837004f232 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -43,7 +43,7 @@ namespace MWPhysics { public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); - static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index a0dde67c2e..ad11878369 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -111,6 +111,106 @@ namespace return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor); } + namespace Visitors + { + struct InitPosition + { + const btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) + { + frameData.mPosition.z() = frameData.mWaterlevel; + MWBase::Environment::get().getWorld()->moveObject(actor->getPtr(), frameData.mPosition, false); + } + frameData.mOldHeight = frameData.mPosition.z(); + const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); + frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z()); + frameData.mInertia = actor->getInertialForce(); + frameData.mStuckFrames = actor->getStuckFrames(); + frameData.mLastStuckPosition = actor->getLastStuckPosition(); + } + }; + + struct PreStep + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); + } + }; + + struct UpdatePosition + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + if (actor->setPosition(frameData.mPosition)) + { + frameData.mPosition = actor->getPosition(); // account for potential position change made by script + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + } + }; + + struct Move + { + const float mPhysicsDt; + const btCollisionWorld* mCollisionWorld; + const MWPhysics::WorldFrameData& mWorldFrameData; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); + } + }; + + struct Sync + { + const bool mAdvanceSimulation; + const float mTimeAccum; + const float mPhysicsDt; + const MWPhysics::PhysicsTaskScheduler* scheduler; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + auto ptr = actor->getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight; + const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround); + + if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + + actor->setSimulationPosition(::interpolateMovements(*actor, frameData, mTimeAccum, mPhysicsDt)); + actor->setLastStuckPosition(frameData.mLastStuckPosition); + actor->setStuckFrames(frameData.mStuckFrames); + if (mAdvanceSimulation) + { + MWWorld::Ptr standingOn; + auto* ptrHolder = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder) + standingOn = ptrHolder->getPtr(); + actor->setStandingOnPtr(standingOn); + // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change + if (actor->getOnGround() == frameData.mWasOnGround) + actor->setOnGround(frameData.mIsOnGround); + actor->setOnSlope(frameData.mIsOnSlope); + actor->setWalkingOnWater(frameData.mWalkingOnWater); + actor->setInertialForce(frameData.mInertia); + } + } + }; + } + namespace Config { /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading and user requested more than 1 background threads @@ -235,13 +335,12 @@ namespace MWPhysics return std::make_tuple(numSteps, actualDelta); } - void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); - assert(actors.size() == actorsData.size()); double timeStart = mTimer->tick(); @@ -259,19 +358,19 @@ namespace MWPhysics timeAccum -= numSteps*newDelta; // init - for (size_t i = 0; i < actors.size(); ++i) + const Visitors::InitPosition vis{mCollisionWorld}; + for (auto& sim : simulations) { - actorsData[i].updatePosition(*actors[i], mCollisionWorld); + std::visit(vis, sim); } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; - mActors = std::move(actors); - mActorsFrameData = std::move(actorsData); + mSimulations = std::move(simulations); mAdvanceSimulation = (mRemainingSteps != 0); mNewFrame = true; - mNumJobs = mActorsFrameData.size(); + mNumJobs = mSimulations.size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); @@ -301,8 +400,7 @@ namespace MWPhysics MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); - mActors.clear(); - mActorsFrameData.clear(); + mSimulations.clear(); for (const auto& [_, actor] : actors) { actor->updatePosition(); @@ -467,47 +565,11 @@ namespace MWPhysics void PhysicsTaskScheduler::updateActorsPositions() { - for (size_t i = 0; i < mActors.size(); ++i) + const Visitors::UpdatePosition vis{mCollisionWorld}; + for (auto& sim : mSimulations) { - if (mActors[i]->setPosition(mActorsFrameData[i].mPosition)) - { - MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - mActorsFrameData[i].mPosition = mActors[i]->getPosition(); // account for potential position change made by script - mActors[i]->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(mActors[i]->getCollisionObject()); - } - } - } - - void PhysicsTaskScheduler::updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const - { - auto ptr = actor.getPtr(); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); - - if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); - else if (heightDiff < 0) - stats.addToFallHeight(-heightDiff); - - actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt)); - actor.setLastStuckPosition(actorData.mLastStuckPosition); - actor.setStuckFrames(actorData.mStuckFrames); - if (simulationPerformed) - { - MWWorld::Ptr standingOn; - auto* ptrHolder = static_cast(getUserPointer(actorData.mStandingOn)); - if (ptrHolder) - standingOn = ptrHolder->getPtr(); - actor.setStandingOnPtr(standingOn); - // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change - if (actor.getOnGround() == actorData.mWasOnGround) - actor.setOnGround(actorData.mIsOnGround); - actor.setOnSlope(actorData.mIsOnSlope); - actor.setWalkingOnWater(actorData.mWalkingOnWater); - actor.setInertialForce(actorData.mInertia); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); + std::visit(vis, sim); } } @@ -532,10 +594,11 @@ namespace MWPhysics { mPreStepBarrier->wait([this] { afterPreStep(); }); int job = 0; + const Visitors::Move vis{mPhysicsDt, mCollisionWorld, *mWorldFrameData}; while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); + std::visit(vis, mSimulations[job]); } mPostStepBarrier->wait([this] { afterPostStep(); }); @@ -577,7 +640,7 @@ namespace MWPhysics void PhysicsTaskScheduler::releaseSharedStates() { std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); - mActors.clear(); + mSimulations.clear(); mUpdateAabb.clear(); } @@ -586,10 +649,11 @@ namespace MWPhysics updateAabbs(); if (!mRemainingSteps) return; - for (size_t i = 0; i < mActors.size(); ++i) + const Visitors::PreStep vis{mCollisionWorld}; + for (auto& sim : mSimulations) { MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); + std::visit(vis, sim); } } @@ -618,7 +682,8 @@ namespace MWPhysics void PhysicsTaskScheduler::syncWithMainThread() { - for (size_t i = 0; i < mActors.size(); ++i) - updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); + const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this}; + for (auto& sim : mSimulations) + std::visit(vis, sim); } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 08997947e4..44330b2cc6 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -39,7 +40,7 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - void applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + void applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); void resetSimulation(const ActorMap& actors); @@ -57,14 +58,12 @@ namespace MWPhysics bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); void debugDraw(); void* getUserPointer(const btCollisionObject* object) const; - void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: void doSimulation(); void worker(); void updateActorsPositions(); - void updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const; bool hasLineOfSight(const Actor* actor1, const Actor* actor2); void refreshLOSCache(); void updateAabbs(); @@ -77,8 +76,7 @@ namespace MWPhysics void syncWithMainThread(); std::unique_ptr mWorldFrameData; - std::vector> mActors; - std::vector mActorsFrameData; + std::vector mSimulations; std::unordered_set mCollisionObjects; float mDefaultPhysicsDt; float mPhysicsDt; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 2a5bcb58bd..f6fc3902e7 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -60,19 +60,6 @@ namespace { - bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world) - { - if (!physicActor) - return false; - const float halfZ = physicActor->getHalfExtents().z(); - const osg::Vec3f actorPosition = physicActor->getPosition(); - const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); - const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); - MWPhysics::ActorTracer tracer; - tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); - return (tracer.mFraction >= 1.0f); - } - void handleJump(const MWWorld::Ptr &ptr) { if (!ptr.getClass().isActor()) @@ -386,7 +373,8 @@ namespace MWPhysics bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) { - return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); + const auto* physactor = getActor(actor); + return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const @@ -727,11 +715,10 @@ namespace MWPhysics actor->setVelocity(osg::Vec3f()); } - std::pair>, std::vector> PhysicsSystem::prepareFrameData(bool willSimulate) + std::vector PhysicsSystem::prepareSimulation(bool willSimulate) { - std::pair>, std::vector> framedata; - framedata.first.reserve(mActors.size()); - framedata.second.reserve(mActors.size()); + std::vector simulations; + simulations.reserve(mActors.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto& [ref, physicActor] : mActors) { @@ -760,14 +747,13 @@ namespace MWPhysics const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - framedata.first.emplace_back(physicActor); - framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel); + simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}}); // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. if (willSimulate) handleJump(ptr); } - return framedata; + return simulations; } void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -793,9 +779,9 @@ namespace MWPhysics mTaskScheduler->resetSimulation(mActors); else { - auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt); + auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt); // modifies mTimeAccum - mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(actors), std::move(framedata), frameStart, frameNumber, stats); + mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(simulations), frameStart, frameNumber, stats); } } @@ -982,7 +968,7 @@ namespace MWPhysics { actor.applyOffsetChange(); mPosition = actor.getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(&actor, mWaterlevel, world)) + if (mWaterCollision && mPosition.z() < mWaterlevel && actor.canMoveToWaterSurface(mWaterlevel, world)) { mPosition.z() = mWaterlevel; MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false); diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 6ec4ebfda9..be9fa10aa6 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -107,6 +108,9 @@ namespace MWPhysics osg::Vec3f mStormDirection; }; + using ActorSimulation = std::pair, ActorFrameData>; + using Simulation = std::variant; + class PhysicsSystem : public RayCastingInterface { public: @@ -253,7 +257,7 @@ namespace MWPhysics void updateWater(); - std::pair>, std::vector> prepareFrameData(bool willSimulate); + std::vector prepareSimulation(bool willSimulate); std::unique_ptr mBroadphase; std::unique_ptr mCollisionConfiguration; From ad7a810a62f669f272ffa55b0609ac611bffcedc Mon Sep 17 00:00:00 2001 From: fredzio Date: Sat, 9 Oct 2021 18:13:54 +0200 Subject: [PATCH 039/110] Unify interface for Actor and Projectile --- apps/openmw/mwphysics/actor.cpp | 27 +------------------ apps/openmw/mwphysics/actor.hpp | 12 --------- apps/openmw/mwphysics/mtphysics.cpp | 2 +- apps/openmw/mwphysics/projectile.cpp | 30 +++++---------------- apps/openmw/mwphysics/projectile.hpp | 7 +---- apps/openmw/mwphysics/ptrholder.hpp | 40 ++++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 68 deletions(-) diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index c7e2308e2f..4857339228 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -23,7 +23,7 @@ namespace MWPhysics Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) , mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents) - , mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} + , mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) @@ -134,11 +134,6 @@ void Actor::setSimulationPosition(const osg::Vec3f& position) mSimulationPosition = position; } -osg::Vec3f Actor::getSimulationPosition() const -{ - return mSimulationPosition; -} - osg::Vec3f Actor::getScaledMeshTranslation() const { return mRotation * osg::componentMultiply(mMeshTranslation, mScale); @@ -192,16 +187,6 @@ void Actor::applyOffsetChange() mWorldPositionChanged = true; } -osg::Vec3f Actor::getPosition() const -{ - return mPosition; -} - -osg::Vec3f Actor::getPreviousPosition() const -{ - return mPreviousPosition; -} - void Actor::setRotation(osg::Quat quat) { std::scoped_lock lock(mPositionMutex); @@ -294,16 +279,6 @@ bool Actor::skipCollisions() return std::exchange(mSkipCollisions, false); } -void Actor::setVelocity(osg::Vec3f velocity) -{ - mVelocity = velocity; -} - -osg::Vec3f Actor::velocity() -{ - return std::exchange(mVelocity, osg::Vec3f()); -} - bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const { const float halfZ = getHalfExtents().z(); diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index d2ebd78379..eb65928dec 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -60,7 +60,6 @@ namespace MWPhysics * to account for e.g. scripted movements */ void setSimulationPosition(const osg::Vec3f& position); - osg::Vec3f getSimulationPosition() const; void updateCollisionObjectPosition(); @@ -95,10 +94,6 @@ namespace MWPhysics // apply position offset. Can't be called during simulation void applyOffsetChange(); - osg::Vec3f getPosition() const; - - osg::Vec3f getPreviousPosition() const; - /** * Returns the half extents of the collision body (scaled according to rendering scale) * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape, @@ -163,9 +158,6 @@ namespace MWPhysics bool skipCollisions(); - void setVelocity(osg::Vec3f velocity); - osg::Vec3f velocity(); - bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; private: @@ -193,11 +185,7 @@ namespace MWPhysics osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mSimulationPosition; - osg::Vec3f mPosition; - osg::Vec3f mPreviousPosition; osg::Vec3f mPositionOffset; - osg::Vec3f mVelocity; bool mWorldPositionChanged; bool mSkipCollisions; bool mSkipSimulation; diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index ad11878369..d3f8bad238 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -546,7 +546,7 @@ namespace MWPhysics } else if (const auto projectile = std::dynamic_pointer_cast(ptr)) { - projectile->commitPositionChange(); + projectile->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); } } diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index 4efb245149..9f8962d5e6 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -31,14 +31,15 @@ Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, f mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); - setPosition(position); + mPosition = position; + mPreviousPosition = position; setCaster(caster); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); - commitPositionChange(); + updateCollisionObjectPosition(); } Projectile::~Projectile() @@ -48,29 +49,12 @@ Projectile::~Projectile() mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } -void Projectile::commitPositionChange() +void Projectile::updateCollisionObjectPosition() { std::scoped_lock lock(mMutex); - if (mTransformUpdatePending) - { - auto& trans = mCollisionObject->getWorldTransform(); - trans.setOrigin(Misc::Convert::toBullet(mPosition)); - mCollisionObject->setWorldTransform(trans); - mTransformUpdatePending = false; - } -} - -void Projectile::setPosition(const osg::Vec3f &position) -{ - std::scoped_lock lock(mMutex); - mPosition = position; - mTransformUpdatePending = true; -} - -osg::Vec3f Projectile::getPosition() const -{ - std::scoped_lock lock(mMutex); - return mPosition; + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); } void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index 5e4e487c03..10ed2c9582 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -36,10 +36,7 @@ namespace MWPhysics btConvexShape* getConvexShape() const { return mConvexShape; } - void commitPositionChange(); - - void setPosition(const osg::Vec3f& position); - osg::Vec3f getPosition() const; + void updateCollisionObjectPosition(); bool isActive() const { @@ -80,13 +77,11 @@ namespace MWPhysics std::unique_ptr mShape; btConvexShape* mConvexShape; - bool mTransformUpdatePending; bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; const btCollisionObject* mCasterColObj; const btCollisionObject* mHitTarget; - osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index e84f3d1cfe..fcd6ce203a 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -30,9 +30,49 @@ namespace MWPhysics return mCollisionObject.get(); } + void setVelocity(osg::Vec3f velocity) + { + mVelocity = velocity; + } + + osg::Vec3f velocity() + { + return std::exchange(mVelocity, osg::Vec3f()); + } + + void setSimulationPosition(const osg::Vec3f& position) + { + mSimulationPosition = position; + } + + osg::Vec3f getSimulationPosition() const + { + return mSimulationPosition; + } + + void setPosition(const osg::Vec3f& position) + { + mPreviousPosition = mPosition; + mPosition = position; + } + + osg::Vec3f getPosition() const + { + return mPosition; + } + + osg::Vec3f getPreviousPosition() const + { + return mPreviousPosition; + } + protected: MWWorld::Ptr mPtr; std::unique_ptr mCollisionObject; + osg::Vec3f mVelocity; + osg::Vec3f mSimulationPosition; + osg::Vec3f mPosition; + osg::Vec3f mPreviousPosition; }; } From a245981e4ec1975731838b2b0663ccead30b79c4 Mon Sep 17 00:00:00 2001 From: fredzio Date: Sat, 9 Oct 2021 18:15:45 +0200 Subject: [PATCH 040/110] Make interpolateMovement works with PtrHolder instead of Actor --- apps/openmw/mwphysics/mtphysics.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index d3f8bad238..525647230f 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -105,10 +105,10 @@ namespace return actorData.mPosition.z() < actorData.mSwimLevel; } - osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); - return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor); + return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); } namespace Visitors @@ -190,7 +190,7 @@ namespace else if (heightDiff < 0) stats.addToFallHeight(-heightDiff); - actor->setSimulationPosition(::interpolateMovements(*actor, frameData, mTimeAccum, mPhysicsDt)); + actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt)); actor->setLastStuckPosition(frameData.mLastStuckPosition); actor->setStuckFrames(frameData.mStuckFrames); if (mAdvanceSimulation) From 3750eb9cd85ca3cbaaa150bda8159d653833bf1d Mon Sep 17 00:00:00 2001 From: fredzio Date: Sat, 9 Oct 2021 18:16:29 +0200 Subject: [PATCH 041/110] Move Projectile simulation to the background thread. --- apps/openmw/mwphysics/movementsolver.cpp | 26 ++++++++++++++ apps/openmw/mwphysics/movementsolver.hpp | 2 ++ apps/openmw/mwphysics/mtphysics.cpp | 22 ++++++++++++ apps/openmw/mwphysics/physicssystem.cpp | 44 +++++++++-------------- apps/openmw/mwphysics/physicssystem.hpp | 14 ++++++-- apps/openmw/mwworld/projectilemanager.cpp | 12 +++---- 6 files changed, 82 insertions(+), 38 deletions(-) diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 29810e9085..0585fe91f3 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,8 @@ #include "constants.hpp" #include "contacttestwrapper.h" #include "physicssystem.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" #include "stepper.hpp" #include "trace.h" @@ -398,6 +401,29 @@ namespace MWPhysics actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate } + void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld) + { + btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition); + btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time); + + if (btFrom == btTo) + return; + + ProjectileConvexCallback resultCallback(projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, projectile.mProjectile); + resultCallback.m_collisionFilterMask = 0xff; + resultCallback.m_collisionFilterGroup = CollisionType_Projectile; + + const btQuaternion btrot = btQuaternion::getIdentity(); + btTransform from_ (btrot, btFrom); + btTransform to_ (btrot, btTo); + + const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape(); + assert(shape->isConvex()); + collisionWorld->convexSweepTest(static_cast(shape), from_, to_, resultCallback); + + projectile.mPosition = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld); + } + btVector3 addMarginToDelta(btVector3 delta) { if(delta.length2() == 0.0) diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 837004f232..1bbe76cbec 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -37,6 +37,7 @@ namespace MWPhysics class Actor; struct ActorFrameData; + struct ProjectileFrameData; struct WorldFrameData; class MovementSolver @@ -44,6 +45,7 @@ namespace MWPhysics public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); + static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 525647230f..a989493f4d 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -133,6 +133,9 @@ namespace frameData.mStuckFrames = actor->getStuckFrames(); frameData.mLastStuckPosition = actor->getLastStuckPosition(); } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } }; struct PreStep @@ -142,6 +145,9 @@ namespace { MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } }; struct UpdatePosition @@ -157,6 +163,13 @@ namespace mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); } } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setPosition(frameData.mPosition); + proj->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(proj->getCollisionObject()); + } }; struct Move @@ -168,6 +181,10 @@ namespace { MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld); + } }; struct Sync @@ -208,6 +225,11 @@ namespace actor->setInertialForce(frameData.mInertia); } } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setSimulationPosition(::interpolateMovements(*proj, mTimeAccum, mPhysicsDt)); + } }; } diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index f6fc3902e7..786378eb8c 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -580,33 +580,6 @@ namespace MWPhysics } } - void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const - { - const auto foundProjectile = mProjectiles.find(projectileId); - assert(foundProjectile != mProjectiles.end()); - auto* projectile = foundProjectile->second.get(); - - btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition()); - btVector3 btTo = Misc::Convert::toBullet(position); - - if (btFrom == btTo) - return; - - ProjectileConvexCallback resultCallback(projectile->getCasterCollisionObject(), projectile->getCollisionObject(), btFrom, btTo, projectile); - resultCallback.m_collisionFilterMask = 0xff; - resultCallback.m_collisionFilterGroup = CollisionType_Projectile; - - const btQuaternion btrot = btQuaternion::getIdentity(); - btTransform from_ (btrot, btFrom); - btTransform to_ (btrot, btTo); - - mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition()); - projectile->setPosition(newpos); - mTaskScheduler->updateSingleAabb(foundProjectile->second); - } - void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) @@ -718,7 +691,7 @@ namespace MWPhysics std::vector PhysicsSystem::prepareSimulation(bool willSimulate) { std::vector simulations; - simulations.reserve(mActors.size()); + simulations.reserve(mActors.size() + mProjectiles.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto& [ref, physicActor] : mActors) { @@ -753,6 +726,12 @@ namespace MWPhysics if (willSimulate) handleJump(ptr); } + + for (const auto& [id, projectile] : mProjectiles) + { + simulations.emplace_back(ProjectileSimulation{projectile, ProjectileFrameData{*projectile}}); + } + return simulations; } @@ -981,6 +960,15 @@ namespace MWPhysics mLastStuckPosition = actor.getLastStuckPosition(); } + ProjectileFrameData::ProjectileFrameData(Projectile& projectile) + : mPosition(projectile.getPosition()) + , mMovement(projectile.velocity()) + , mCaster(projectile.getCasterCollisionObject()) + , mCollisionObject(projectile.getCollisionObject()) + , mProjectile(&projectile) + { + } + WorldFrameData::WorldFrameData() : mIsInStorm(MWBase::Environment::get().getWorld()->isInStorm()) , mStormDirection(MWBase::Environment::get().getWorld()->getStormDirection()) diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index be9fa10aa6..a025f1cc8b 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -101,6 +101,16 @@ namespace MWPhysics const bool mSkipCollisionDetection; }; + struct ProjectileFrameData + { + explicit ProjectileFrameData(Projectile& projectile); + osg::Vec3f mPosition; + osg::Vec3f mMovement; + const btCollisionObject* mCaster; + const btCollisionObject* mCollisionObject; + Projectile* mProjectile; + }; + struct WorldFrameData { WorldFrameData(); @@ -109,7 +119,8 @@ namespace MWPhysics }; using ActorSimulation = std::pair, ActorFrameData>; - using Simulation = std::variant; + using ProjectileSimulation = std::pair, ProjectileFrameData>; + using Simulation = std::variant; class PhysicsSystem : public RayCastingInterface { @@ -128,7 +139,6 @@ namespace MWPhysics int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); - void updateProjectile(const int projectileId, const osg::Vec3f &position) const; void removeProjectile(const int projectileId); void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 9ee137fab6..ea1d6bde1a 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -432,7 +432,7 @@ namespace MWWorld float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; osg::Vec3f direction = orient * osg::Vec3f(0,1,0); direction.normalize(); - osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed; + projectile->setVelocity(direction * speed); update(magicBoltState, duration); @@ -441,8 +441,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); } } @@ -460,7 +458,7 @@ namespace MWWorld // simulating aerodynamics at all projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration; + projectile->setVelocity(projectileState.mVelocity); // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. if (!projectileState.mThrown) @@ -479,8 +477,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(projectileState.mProjectileId, newPos); } } @@ -493,7 +489,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) @@ -529,7 +525,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); From 1a51c6eb5dfe4e4a95fa9ec99a9a16ce9f0b6bd3 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 27 Oct 2021 23:40:54 +0200 Subject: [PATCH 042/110] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 13196a4aa6..49f3f4b3bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,7 +707,7 @@ endif() if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") - set_target_properties(components openmw PROPERTIES COMPILE_DEFINITIONS "GL_SILENCE_DEPRECATION=1") + add_compile_definitions(GL_SILENCE_DEPRECATION=1) endif() # Apple bundling From fa5581942eb5019f84b866f038741d4ff9b692ba Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 27 Oct 2021 23:54:04 +0200 Subject: [PATCH 043/110] Update CMakeLists.txt --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 49f3f4b3bf..1b4e5609ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,7 +707,8 @@ endif() if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") - add_compile_definitions(GL_SILENCE_DEPRECATION=1) + target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1) + target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1) endif() # Apple bundling From 9b565c4cf9413af744fb4224cd66e9ab8a3ad749 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 22:16:43 +0200 Subject: [PATCH 044/110] Remove dead code --- apps/opencs/model/world/scriptcontext.cpp | 5 ----- apps/opencs/model/world/scriptcontext.hpp | 3 --- apps/openmw/mwscript/compilercontext.cpp | 10 ---------- apps/openmw/mwscript/compilercontext.hpp | 3 --- components/compiler/context.hpp | 3 --- 5 files changed, 24 deletions(-) diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 344ae322e9..9b465d7ffb 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -102,11 +102,6 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); } -bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const -{ - return mData.getJournals().searchId (name)!=-1; -} - void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; diff --git a/apps/opencs/model/world/scriptcontext.hpp b/apps/opencs/model/world/scriptcontext.hpp index 8e1a5e57b8..cb08fc70bd 100644 --- a/apps/opencs/model/world/scriptcontext.hpp +++ b/apps/opencs/model/world/scriptcontext.hpp @@ -39,9 +39,6 @@ namespace CSMWorld bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? - void invalidateIds(); void clear(); diff --git a/apps/openmw/mwscript/compilercontext.cpp b/apps/openmw/mwscript/compilercontext.cpp index 4a7038e1cb..983365e06a 100644 --- a/apps/openmw/mwscript/compilercontext.cpp +++ b/apps/openmw/mwscript/compilercontext.cpp @@ -86,14 +86,4 @@ namespace MWScript store.get().search (name) || store.get().search (name); } - - bool CompilerContext::isJournalId (const std::string& name) const - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Dialogue *topic = store.get().search (name); - - return topic && topic->mType==ESM::Dialogue::Journal; - } } diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 00b10ea06d..d800781fd8 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -39,9 +39,6 @@ namespace MWScript bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? }; } diff --git a/components/compiler/context.hpp b/components/compiler/context.hpp index 399e8125bb..d3caba7c53 100644 --- a/components/compiler/context.hpp +++ b/components/compiler/context.hpp @@ -42,9 +42,6 @@ namespace Compiler virtual bool isId (const std::string& name) const = 0; ///< Does \a name match an ID, that can be referenced? - - virtual bool isJournalId (const std::string& name) const = 0; - ///< Does \a name match a journal ID? }; } From 37386f417ed481bc682ffa11359b073dc334f6d3 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Fri, 29 Oct 2021 20:09:47 +0200 Subject: [PATCH 045/110] Support *.omwscripts in openmw-launcher --- apps/launcher/datafilespage.cpp | 2 +- apps/launcher/utils/cellnameloader.cpp | 2 ++ apps/opencs/view/doc/filedialog.cpp | 2 +- components/contentselector/model/contentmodel.cpp | 14 +++++++++++++- components/contentselector/model/contentmodel.hpp | 3 ++- .../contentselector/view/contentselector.cpp | 8 ++++---- .../contentselector/view/contentselector.hpp | 4 ++-- 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 24729d5096..4c66668e4a 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -32,7 +32,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: { ui.setupUi (this); setObjectName ("DataFilesPage"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/true); const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index 594946394c..e8ba54651c 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -10,6 +10,8 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) // Loop through all content files for (auto &contentPath : contentPaths) { + if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) + continue; esmReader.open(contentPath.toStdString()); // Loop through all records diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 8ff063ed31..c3d0d8cc50 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -24,7 +24,7 @@ CSVDoc::FileDialog::FileDialog(QWidget *parent) : resize(400, 400); setObjectName ("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget (this); } diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 690f968142..208b1315f3 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -9,9 +9,10 @@ #include -ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) : +ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts) : QAbstractTableModel(parent), mWarningIcon(warningIcon), + mShowOMWScripts(showOMWScripts), mMimeType ("application/omwcontent"), mMimeTypes (QStringList() << mMimeType), mColumnCount (1), @@ -416,6 +417,8 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) QDir dir(path); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + if (mShowOMWScripts) + filters << "*.omwscripts"; dir.setNameFilters(filters); for (const QString &path2 : dir.entryList()) @@ -425,6 +428,15 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) if (item(info.fileName())) continue; + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) + { + EsmFile *file = new EsmFile(path2); + file->setDate(info.lastModified()); + file->setFilePath(info.absoluteFilePath()); + addFile(file); + continue; + } + try { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder = diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index d245a0dcbf..f8130e3649 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorModel { Q_OBJECT public: - explicit ContentModel(QObject *parent, QIcon warningIcon); + explicit ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts); ~ContentModel(); void setEncoding(const QString &encoding); @@ -84,6 +84,7 @@ namespace ContentSelectorModel QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; + bool mShowOMWScripts; public: diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index d7996dfae3..f18e80dd0a 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -10,21 +10,21 @@ #include #include -ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : +ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent, bool showOMWScripts) : QObject(parent) { ui.setupUi(parent); ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); - buildContentModel(); + buildContentModel(showOMWScripts); buildGameFileView(); buildAddonView(); } -void ContentSelectorView::ContentSelector::buildContentModel() +void ContentSelectorView::ContentSelector::buildContentModel(bool showOMWScripts) { QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); - mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon); + mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } void ContentSelectorView::ContentSelector::buildGameFileView() diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index cda68fa1b7..4a9983c1bf 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorView public: - explicit ContentSelector(QWidget *parent = nullptr); + explicit ContentSelector(QWidget *parent = nullptr, bool showOMWScripts = false); QString currentFile() const; @@ -56,7 +56,7 @@ namespace ContentSelectorView Ui::ContentSelector ui; - void buildContentModel(); + void buildContentModel(bool showOMWScripts); void buildGameFileView(); void buildAddonView(); void buildContextMenu(); From 29a772c33fcc3de36aaa1d0d9517ab1e38010a8b Mon Sep 17 00:00:00 2001 From: elsid Date: Thu, 28 Oct 2021 01:43:25 +0200 Subject: [PATCH 046/110] Rename Resource::BulletShape::CollisionBox fields according to styleguide --- apps/openmw/mwphysics/actor.cpp | 2 +- apps/openmw/mwphysics/physicssystem.cpp | 4 +-- .../nifloader/testbulletnifloader.cpp | 36 +++++++++---------- components/nifbullet/bulletnifloader.cpp | 8 ++--- components/resource/bulletshape.hpp | 4 +-- components/resource/bulletshapemanager.cpp | 8 ++--- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 4857339228..9d02d310b0 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -22,7 +22,7 @@ namespace MWPhysics Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) - , mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents) + , mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents) , mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5b00677fce..636c3492f7 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -618,7 +618,7 @@ namespace MWPhysics osg::ref_ptr shape = mShapeManager->getShape(mesh); // Try to get shape from basic model as fallback for creatures - if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) @@ -643,7 +643,7 @@ namespace MWPhysics { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); - float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f; + float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; mProjectileId++; diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index a1f5d6091d..d0a5b3d2e5 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -162,8 +162,8 @@ namespace Resource { return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) - && lhs.mCollisionBox.extents == rhs.mCollisionBox.extents - && lhs.mCollisionBox.center == rhs.mCollisionBox.center + && lhs.mCollisionBox.mExtents == rhs.mCollisionBox.mExtents + && lhs.mCollisionBox.mCenter == rhs.mCollisionBox.mCenter && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } @@ -172,8 +172,8 @@ namespace Resource return stream << "Resource::BulletShape {" << value.mCollisionShape << ", " << value.mAvoidCollisionShape << ", " - << "osg::Vec3f {" << value.mCollisionBox.extents << "}" << ", " - << "osg::Vec3f {" << value.mCollisionBox.center << "}" << ", " + << "osg::Vec3f {" << value.mCollisionBox.mExtents << "}" << ", " + << "osg::Vec3f {" << value.mCollisionBox.mCenter << "}" << ", " << value.mAnimatedShapes << "}"; } @@ -441,8 +441,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -466,8 +466,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -496,8 +496,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -531,8 +531,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -566,8 +566,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(4, 5, 6); - expected.mCollisionBox.center = osg::Vec3f(-4, -5, -6); + expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); + expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6); std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); @@ -589,8 +589,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -623,8 +623,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 6ae1759395..03ef63014b 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -143,8 +143,8 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) { if (findBoundingBox(node, filename)) { - const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.extents); - const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.center); + const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); + const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); std::unique_ptr compound (new btCompoundShape); std::unique_ptr boxShape(new btBoxShape(extents)); btTransform transform = btTransform::getIdentity(); @@ -206,8 +206,8 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& switch (type) { case Nif::NiBoundingVolume::Type::BOX_BV: - mShape->mCollisionBox.extents = node->bounds.box.extents; - mShape->mCollisionBox.center = node->bounds.box.center; + mShape->mCollisionBox.mExtents = node->bounds.box.extents; + mShape->mCollisionBox.mCenter = node->bounds.box.center; break; default: { diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 6ac8064cb3..b40ab6b6a7 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -29,8 +29,8 @@ namespace Resource struct CollisionBox { - osg::Vec3f extents; - osg::Vec3f center; + osg::Vec3f mExtents; + osg::Vec3f mCenter; }; // Used for actors and projectiles. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. // For now, use one file <-> one resource for simplicity. diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index 5b6dce067c..d295265b5f 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -86,10 +86,10 @@ public: btBvhTriangleMeshShape* triangleMeshShape = new TriangleMeshShape(mTriangleMesh.release(), true); btVector3 aabbMin = triangleMeshShape->getLocalAabbMin(); btVector3 aabbMax = triangleMeshShape->getLocalAabbMax(); - shape->mCollisionBox.extents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; - shape->mCollisionBox.extents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; - shape->mCollisionBox.extents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; - shape->mCollisionBox.center = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, + shape->mCollisionBox.mExtents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; + shape->mCollisionBox.mExtents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; + shape->mCollisionBox.mExtents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; + shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, (aabbMax[1] + aabbMin[1]) / 2.0f, (aabbMax[2] + aabbMin[2]) / 2.0f ); shape->mCollisionShape = triangleMeshShape; From 8c0102e1eeb1679f0a55df7683e3145f534fc27f Mon Sep 17 00:00:00 2001 From: wareya Date: Fri, 29 Oct 2021 15:11:08 -0400 Subject: [PATCH 047/110] Optimize short-trace test to only be done when particularly helpful --- apps/openmw/mwphysics/movementsolver.cpp | 4 +- apps/openmw/mwphysics/stepper.cpp | 4 +- apps/openmw/mwphysics/trace.cpp | 50 +++++++++++++++--------- apps/openmw/mwphysics/trace.h | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 44a5391f0d..28570e6687 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -203,7 +203,7 @@ namespace MWPhysics if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -338,7 +338,7 @@ namespace MWPhysics osg::Vec3f from = newPosition; auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); - tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); if(tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index af85658910..5ef6833701 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -36,7 +36,7 @@ namespace MWPhysics // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. - mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld); + mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); float upDistance = 0; if(!mUpStepper.mHitObject) @@ -117,7 +117,7 @@ namespace MWPhysics downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; - mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround); // can't step down onto air, non-walkable-slopes, or actors // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index b2c5410a62..e489ea0eb2 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -24,45 +24,57 @@ ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& assert(shape->isConvex()); const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too - ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world); + ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; + traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; if(actorFilter) - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; + traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - world->convexSweepTest(static_cast(shape), transFrom, transTo, newTraceCallback); - return newTraceCallback; + world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); + return traceCallback; } -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace) { const btVector3 btstart = Misc::Convert::toBullet(start); btVector3 btend = Misc::Convert::toBullet(end); - bool do_fallback = false; - if((btend-btstart).length2() > 5.0*5.0) + // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests + // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be + // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. + // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. + // This trace needs to be at least a couple units long, but there's no one particular ideal length. + // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. + // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) + const float fallback_length = 2.1f; + bool doing_short_trace = false; + // For some reason, typical scenes perform a little better if we increase the threshold length for the length test. + // (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks this was + // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) + if(attempt_short_trace && (btend-btstart).length2() > fallback_length*fallback_length*2.0) { - btend = btstart + (btend-btstart).normalized()*5.0; - do_fallback = true; + btend = btstart + (btend-btstart).normalized()*fallback_length; + doing_short_trace = true; } - auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - if((end-start).length2() > 0.0) + mFraction = traceCallback.m_closestHitFraction; + // ensure fraction is correct (covers intended distance traveled instead of actual distance traveled) + if(doing_short_trace && (end-start).length2() > 0.0) mFraction *= (btend-btstart).length() / (end-start).length(); - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; - mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); - mHitObject = newTraceCallback.m_hitCollisionObject; + mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld); + mHitObject = traceCallback.m_hitCollisionObject; } else { - if(do_fallback) + if(doing_short_trace) { btend = Misc::Convert::toBullet(end); auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); diff --git a/apps/openmw/mwphysics/trace.h b/apps/openmw/mwphysics/trace.h index 0297c9e076..af38756b3e 100644 --- a/apps/openmw/mwphysics/trace.h +++ b/apps/openmw/mwphysics/trace.h @@ -20,7 +20,7 @@ namespace MWPhysics float mFraction; - void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); + void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace = false); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } From 68f4c336ce5db928efb7e09ee3c988a3eaba45f1 Mon Sep 17 00:00:00 2001 From: fredzio Date: Tue, 19 Oct 2021 18:36:30 +0200 Subject: [PATCH 048/110] Rework again SetPos command to make more mods work. Previous version skipped collision the frame immediately after a call to SetPos. It worked for one-off calls (teleports for instance) and continuous call along a pre-defined path (scenic travel). However, in the case of mod which uses SetPos to simulate a player-controlled movement, it is equivalent to using tcl. Solution: 1/ skip update of mPosition and mPreviousPosition to avoid janky interpolation 2/ use back plain moveObject() instead of moveObjectBy() since we don't want physics simulation 3/ rework a little bit waterwalking influence on coordinate because of 1/ --- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwphysics/actor.cpp | 18 +++++++----------- apps/openmw/mwphysics/actor.hpp | 5 +---- apps/openmw/mwphysics/physicssystem.cpp | 7 ++++--- .../mwscript/transformationextensions.cpp | 8 ++++---- apps/openmw/mwworld/worldimp.cpp | 6 +++--- apps/openmw/mwworld/worldimp.hpp | 2 +- 7 files changed, 21 insertions(+), 27 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 8b33095fde..c1a8d09120 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -289,7 +289,7 @@ namespace MWBase virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 4857339228..4e87f5384f 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -124,7 +124,6 @@ void Actor::updatePosition() mSimulationPosition = worldPosition; mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; - mSkipCollisions = true; mSkipSimulation = true; } @@ -163,17 +162,19 @@ bool Actor::setPosition(const osg::Vec3f& position) { std::scoped_lock lock(mPositionMutex); applyOffsetChange(); - bool hasChanged = mPosition != position || mWorldPositionChanged; - mPreviousPosition = mPosition; - mPosition = position; + bool hasChanged = (mPosition != position && !mSkipSimulation) || mWorldPositionChanged; + if (!mSkipSimulation) + { + mPreviousPosition = mPosition; + mPosition = position; + } return hasChanged; } -void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) +void Actor::adjustPosition(const osg::Vec3f& offset) { std::scoped_lock lock(mPositionMutex); mPositionOffset += offset; - mSkipCollisions = mSkipCollisions || ignoreCollisions; } void Actor::applyOffsetChange() @@ -274,11 +275,6 @@ void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) mStandingOnPtr = ptr; } -bool Actor::skipCollisions() -{ - return std::exchange(mSkipCollisions, false); -} - bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const { const float halfZ = getHalfExtents().z(); diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index eb65928dec..01d8037f6b 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -89,7 +89,7 @@ namespace MWPhysics void updatePosition(); // register a position offset that will be applied during simulation. - void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions); + void adjustPosition(const osg::Vec3f& offset); // apply position offset. Can't be called during simulation void applyOffsetChange(); @@ -156,8 +156,6 @@ namespace MWPhysics mLastStuckPosition = position; } - bool skipCollisions(); - bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; private: @@ -187,7 +185,6 @@ namespace MWPhysics osg::Vec3f mScale; osg::Vec3f mPositionOffset; bool mWorldPositionChanged; - bool mSkipCollisions; bool mSkipSimulation; mutable std::mutex mPositionMutex; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5b00677fce..af6dc109ed 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -941,7 +941,7 @@ namespace MWPhysics , mWasOnGround(actor.getOnGround()) , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mWaterCollision(waterCollision) - , mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode()) + , mSkipCollisionDetection(!actor.getCollisionMode()) { } @@ -951,8 +951,9 @@ namespace MWPhysics mPosition = actor.getPosition(); if (mWaterCollision && mPosition.z() < mWaterlevel && actor.canMoveToWaterSurface(mWaterlevel, world)) { - mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false); + MWBase::Environment::get().getWorld()->moveObjectBy(actor.getPtr(), osg::Vec3f(0, 0, mWaterlevel - mPosition.z())); + actor.applyOffsetChange(); + mPosition = actor.getPosition(); } mOldHeight = mPosition.z(); const auto rotation = actor.getPtr().getRefData().getPosition().asRotationVec3(); diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 3d00b24ef8..5cfb2b2989 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -32,7 +32,7 @@ namespace MWScript std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) - MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false, false); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); } template @@ -312,7 +312,7 @@ namespace MWScript } dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true, true)); + MWBase::Environment::get().getWorld()->moveObject(ptr, newPos, true, true)); } }; @@ -731,7 +731,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -767,7 +767,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 21e2d8380a..e860a114c6 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1258,14 +1258,14 @@ namespace MWWorld return moveObject(ptr, cell, position, movePhysics); } - MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) { auto* actor = mPhysics->getActor(ptr); osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) - actor->adjustPosition(vec, ignoreCollisions); + actor->adjustPosition(vec); if (ptr.getClass().isActor()) - return moveObject(ptr, newpos, false, moveToActive && ptr != getPlayerPtr()); + return moveObject(ptr, newpos, false, ptr != getPlayerPtr()); return moveObject(ptr, newpos); } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6e48f045c0..a795b4eafd 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -376,7 +376,7 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr - MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; From ca8584f6f61ca277272000fad724506e55d7a45c Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 02:58:40 +0200 Subject: [PATCH 049/110] Move functions independent from BulletShape into anonymous namespace --- components/resource/bulletshape.cpp | 112 +++++++++++++--------------- components/resource/bulletshape.hpp | 6 -- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 798a6778e6..5e0415e59e 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -10,6 +10,53 @@ namespace Resource { +namespace +{ + btCollisionShape* duplicateCollisionShape(const btCollisionShape *shape) + { + if (shape == nullptr) + return nullptr; + + if (shape->isCompound()) + { + const btCompoundShape *comp = static_cast(shape); + btCompoundShape* newShape = new btCompoundShape; + + for (int i = 0, n = comp->getNumChildShapes(); i < n; ++i) + { + btCollisionShape* child = duplicateCollisionShape(comp->getChildShape(i)); + const btTransform& trans = comp->getChildTransform(i); + newShape->addChildShape(trans, child); + } + + return newShape; + } + + if (const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) + return new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); + + if (const btBoxShape* boxshape = dynamic_cast(shape)) + return new btBoxShape(*boxshape); + + if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) + return new btHeightfieldTerrainShape(static_cast(*shape)); + + throw std::logic_error(std::string("Unhandled Bullet shape duplication: ") + shape->getName()); + } + + void deleteShape(btCollisionShape* shape) + { + if (shape->isCompound()) + { + btCompoundShape* compound = static_cast(shape); + for (int i = 0, n = compound->getNumChildShapes(); i < n; i++) + if (btCollisionShape* child = compound->getChildShape(i)) + deleteShape(child); + } + + delete shape; + } +} BulletShape::BulletShape() : mCollisionShape(nullptr) @@ -28,58 +75,10 @@ BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) BulletShape::~BulletShape() { - deleteShape(mAvoidCollisionShape); - deleteShape(mCollisionShape); -} - -void BulletShape::deleteShape(btCollisionShape* shape) -{ - if(shape!=nullptr) - { - if(shape->isCompound()) - { - btCompoundShape* ms = static_cast(shape); - int a = ms->getNumChildShapes(); - for(int i=0; i getChildShape(i)); - } - delete shape; - } -} - -btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *shape) const -{ - if(shape->isCompound()) - { - const btCompoundShape *comp = static_cast(shape); - btCompoundShape *newShape = new btCompoundShape; - - int numShapes = comp->getNumChildShapes(); - for(int i = 0;i < numShapes;++i) - { - btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i)); - const btTransform& trans = comp->getChildTransform(i); - newShape->addChildShape(trans, child); - } - - return newShape; - } - - if(const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) - { - btScaledBvhTriangleMeshShape* newShape = new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); - return newShape; - } - - if (const btBoxShape* boxshape = dynamic_cast(shape)) - { - return new btBoxShape(*boxshape); - } - - if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) - return new btHeightfieldTerrainShape(static_cast(*shape)); - - throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName()); + if (mAvoidCollisionShape != nullptr) + deleteShape(mAvoidCollisionShape); + if (mCollisionShape != nullptr) + deleteShape(mCollisionShape); } btCollisionShape *BulletShape::getCollisionShape() const @@ -115,14 +114,9 @@ BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) , mSource(source) { mCollisionBox = source->mCollisionBox; - mAnimatedShapes = source->mAnimatedShapes; - - if (source->mCollisionShape) - mCollisionShape = duplicateCollisionShape(source->mCollisionShape); - - if (source->mAvoidCollisionShape) - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); + mCollisionShape = duplicateCollisionShape(source->mCollisionShape); + mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); } } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index b40ab6b6a7..8b30464fe2 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -44,8 +44,6 @@ namespace Resource osg::ref_ptr makeInstance() const; - btCollisionShape* duplicateCollisionShape(const btCollisionShape* shape) const; - btCollisionShape* getCollisionShape() const; btCollisionShape* getAvoidCollisionShape() const; @@ -53,10 +51,6 @@ namespace Resource void setLocalScaling(const btVector3& scale); bool isAnimated() const; - - private: - - void deleteShape(btCollisionShape* shape); }; From 80e3623d9a3c77d6896e9c5787a49ad45256d450 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:04:54 +0200 Subject: [PATCH 050/110] Avoid dynamic cast in duplicateCollisionShape --- components/resource/bulletshape.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 5e0415e59e..3f599e5386 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -32,11 +32,17 @@ namespace return newShape; } - if (const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) + if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) + { + const btBvhTriangleMeshShape* trishape = static_cast(shape); return new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); + } - if (const btBoxShape* boxshape = dynamic_cast(shape)) + if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) + { + const btBoxShape* boxshape = static_cast(shape); return new btBoxShape(*boxshape); + } if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) return new btHeightfieldTerrainShape(static_cast(*shape)); From b905dd17c3e27f9bee9143a27fafcfdf84e6902b Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:06:22 +0200 Subject: [PATCH 051/110] Use unique_ptr to store btCollisionShape in BulletShape --- .../detournavigator/navigator.cpp | 6 +-- .../nifloader/testbulletnifloader.cpp | 42 +++++++++--------- components/nifbullet/bulletnifloader.cpp | 8 ++-- components/resource/bulletshape.cpp | 43 +++++++------------ components/resource/bulletshape.hpp | 14 ++++-- components/resource/bulletshapemanager.cpp | 2 +- 6 files changed, 55 insertions(+), 60 deletions(-) diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 62d3f9de04..6579076778 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -117,7 +117,7 @@ namespace osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) { osg::ref_ptr bulletShape(new Resource::BulletShape); - bulletShape->mCollisionShape = std::move(shape).release(); + bulletShape->mCollisionShape.reset(std::move(shape).release()); return new Resource::BulletShapeInstance(bulletShape); } @@ -466,7 +466,7 @@ namespace }}; std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); shapePtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mCollisionShape = shapePtr.release(); + bulletShape->mCollisionShape.reset(shapePtr.release()); std::array heightfieldDataAvoid {{ -25, -25, -25, -25, -25, @@ -477,7 +477,7 @@ namespace }}; std::unique_ptr shapeAvoidPtr = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mAvoidCollisionShape = shapeAvoidPtr.release(); + bulletShape->mAvoidCollisionShape.reset(shapeAvoidPtr.release()); osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index d0a5b3d2e5..3d4628c267 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -160,8 +160,8 @@ namespace Resource { static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs) { - return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) - && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) + return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) + && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) && lhs.mCollisionBox.mExtents == rhs.mCollisionBox.mExtents && lhs.mCollisionBox.mCenter == rhs.mCollisionBox.mCenter && lhs.mAnimatedShapes == rhs.mAnimatedShapes; @@ -170,8 +170,8 @@ namespace Resource static std::ostream& operator <<(std::ostream& stream, const Resource::BulletShape& value) { return stream << "Resource::BulletShape {" - << value.mCollisionShape << ", " - << value.mAvoidCollisionShape << ", " + << value.mCollisionShape.get() << ", " + << value.mAvoidCollisionShape.get() << ", " << "osg::Vec3f {" << value.mCollisionBox.mExtents << "}" << ", " << "osg::Vec3f {" << value.mCollisionBox.mCenter << "}" << ", " << value.mAnimatedShapes @@ -446,7 +446,7 @@ namespace std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -471,7 +471,7 @@ namespace std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -501,7 +501,7 @@ namespace std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -536,7 +536,7 @@ namespace std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -571,7 +571,7 @@ namespace std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -605,7 +605,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -641,7 +641,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -659,7 +659,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -680,7 +680,7 @@ namespace triangles->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -698,7 +698,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -720,7 +720,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -746,7 +746,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -784,7 +784,7 @@ namespace shape->addChildShape(mResultTransform, mesh.release()); shape->addChildShape(mResultTransform, mesh2.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -813,7 +813,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -854,7 +854,7 @@ namespace shape->addChildShape(mResultTransform2, mesh2.release()); shape->addChildShape(btTransform::getIdentity(), mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -873,7 +873,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false); + expected.mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), false)); EXPECT_EQ(*result, expected); } @@ -979,7 +979,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 03ef63014b..685a80951a 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -152,7 +152,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) compound->addChildShape(transform, boxShape.get()); boxShape.release(); - mShape->mCollisionShape = compound.release(); + mShape->mCollisionShape.reset(compound.release()); return mShape; } } @@ -179,17 +179,17 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) child.release(); mStaticMesh.release(); } - mShape->mCollisionShape = mCompoundShape.release(); + mShape->mCollisionShape.reset(mCompoundShape.release()); } else if (mStaticMesh) { - mShape->mCollisionShape = new Resource::TriangleMeshShape(mStaticMesh.get(), true); + mShape->mCollisionShape.reset(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); mStaticMesh.release(); } if (mAvoidStaticMesh) { - mShape->mAvoidCollisionShape = new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false); + mShape->mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false)); mAvoidStaticMesh.release(); } diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 3f599e5386..e52e68ca07 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -12,7 +12,7 @@ namespace Resource { namespace { - btCollisionShape* duplicateCollisionShape(const btCollisionShape *shape) + CollisionShapePtr duplicateCollisionShape(const btCollisionShape *shape) { if (shape == nullptr) return nullptr; @@ -20,13 +20,13 @@ namespace if (shape->isCompound()) { const btCompoundShape *comp = static_cast(shape); - btCompoundShape* newShape = new btCompoundShape; + std::unique_ptr newShape(new btCompoundShape); for (int i = 0, n = comp->getNumChildShapes(); i < n; ++i) { - btCollisionShape* child = duplicateCollisionShape(comp->getChildShape(i)); + auto child = duplicateCollisionShape(comp->getChildShape(i)); const btTransform& trans = comp->getChildTransform(i); - newShape->addChildShape(trans, child); + newShape->addChildShape(trans, child.release()); } return newShape; @@ -35,17 +35,17 @@ namespace if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) { const btBvhTriangleMeshShape* trishape = static_cast(shape); - return new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); + return CollisionShapePtr(new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f))); } if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) { const btBoxShape* boxshape = static_cast(shape); - return new btBoxShape(*boxshape); + return CollisionShapePtr(new btBoxShape(*boxshape)); } if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) - return new btHeightfieldTerrainShape(static_cast(*shape)); + return CollisionShapePtr(new btHeightfieldTerrainShape(static_cast(*shape))); throw std::logic_error(std::string("Unhandled Bullet shape duplication: ") + shape->getName()); } @@ -64,37 +64,27 @@ namespace } } -BulletShape::BulletShape() - : mCollisionShape(nullptr) - , mAvoidCollisionShape(nullptr) +void DeleteCollisionShape::operator()(btCollisionShape* shape) const { - + deleteShape(shape); } BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) - : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape)) - , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape)) + : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape.get())) + , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape.get())) , mCollisionBox(copy.mCollisionBox) , mAnimatedShapes(copy.mAnimatedShapes) { } -BulletShape::~BulletShape() -{ - if (mAvoidCollisionShape != nullptr) - deleteShape(mAvoidCollisionShape); - if (mCollisionShape != nullptr) - deleteShape(mCollisionShape); -} - btCollisionShape *BulletShape::getCollisionShape() const { - return mCollisionShape; + return mCollisionShape.get(); } btCollisionShape *BulletShape::getAvoidCollisionShape() const { - return mAvoidCollisionShape; + return mAvoidCollisionShape.get(); } void BulletShape::setLocalScaling(const btVector3& scale) @@ -116,13 +106,12 @@ osg::ref_ptr BulletShape::makeInstance() const } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) - : BulletShape() - , mSource(source) + : mSource(source) { mCollisionBox = source->mCollisionBox; mAnimatedShapes = source->mAnimatedShapes; - mCollisionShape = duplicateCollisionShape(source->mCollisionShape); - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); + mCollisionShape = duplicateCollisionShape(source->mCollisionShape.get()); + mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape.get()); } } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 8b30464fe2..369aed18a0 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #include +#include #include #include @@ -13,19 +14,24 @@ class btCollisionShape; namespace Resource { + struct DeleteCollisionShape + { + void operator()(btCollisionShape* shape) const; + }; + + using CollisionShapePtr = std::unique_ptr; class BulletShapeInstance; class BulletShape : public osg::Object { public: - BulletShape(); + BulletShape() = default; BulletShape(const BulletShape& copy, const osg::CopyOp& copyop); - virtual ~BulletShape(); META_Object(Resource, BulletShape) - btCollisionShape* mCollisionShape; - btCollisionShape* mAvoidCollisionShape; + CollisionShapePtr mCollisionShape; + CollisionShapePtr mAvoidCollisionShape; struct CollisionBox { diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index d295265b5f..cde069837a 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -92,7 +92,7 @@ public: shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, (aabbMax[1] + aabbMin[1]) / 2.0f, (aabbMax[2] + aabbMin[2]) / 2.0f ); - shape->mCollisionShape = triangleMeshShape; + shape->mCollisionShape.reset(triangleMeshShape); return shape; } From fc9a405dc5897f335c5317b36296cbffa2cf57b0 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:16:21 +0200 Subject: [PATCH 052/110] Make BulletShape::makeInstance free function --- components/resource/bulletshape.cpp | 5 ++--- components/resource/bulletshape.hpp | 5 ++--- components/resource/bulletshapemanager.cpp | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index e52e68ca07..4b074ee146 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -99,10 +99,9 @@ bool BulletShape::isAnimated() const return !mAnimatedShapes.empty(); } -osg::ref_ptr BulletShape::makeInstance() const +osg::ref_ptr makeInstance(osg::ref_ptr source) { - osg::ref_ptr instance (new BulletShapeInstance(this)); - return instance; + return {new BulletShapeInstance(std::move(source))}; } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 369aed18a0..bf249cb364 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -21,7 +21,6 @@ namespace Resource using CollisionShapePtr = std::unique_ptr; - class BulletShapeInstance; class BulletShape : public osg::Object { public: @@ -48,8 +47,6 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - osg::ref_ptr makeInstance() const; - btCollisionShape* getCollisionShape() const; btCollisionShape* getAvoidCollisionShape() const; @@ -71,6 +68,8 @@ namespace Resource osg::ref_ptr mSource; }; + osg::ref_ptr makeInstance(osg::ref_ptr source); + // Subclass btBhvTriangleMeshShape to auto-delete the meshInterface struct TriangleMeshShape : public btBvhTriangleMeshShape { diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index cde069837a..ae6f3659a5 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -193,9 +193,8 @@ osg::ref_ptr BulletShapeManager::createInstance(const std:: { osg::ref_ptr shape = getShape(name); if (shape) - return shape->makeInstance(); - else - return osg::ref_ptr(); + return makeInstance(std::move(shape)); + return osg::ref_ptr(); } void BulletShapeManager::updateCache(double referenceTime) From 8e71c246bfcae5c44907c4a06702f2403082b4da Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:30:38 +0200 Subject: [PATCH 053/110] Remove redundant BulletShape getters --- apps/openmw/mwphysics/object.cpp | 6 +++--- apps/openmw/mwphysics/physicssystem.cpp | 2 +- apps/openmw/mwworld/scene.cpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 2 +- apps/openmw_test_suite/detournavigator/navigator.cpp | 2 +- components/detournavigator/navigatorimpl.cpp | 8 ++++---- components/resource/bulletshape.cpp | 10 ---------- components/resource/bulletshape.hpp | 4 ---- 8 files changed, 11 insertions(+), 25 deletions(-) diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 879c12124e..08fcc7e47d 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -24,7 +24,7 @@ namespace MWPhysics , mTaskScheduler(scheduler) { mPtr = ptr; - mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->getCollisionShape(), + mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); mCollisionObject->setUserPointer(this); mShapeInstance->setLocalScaling(mScale); @@ -109,9 +109,9 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; - assert (mShapeInstance->getCollisionShape()->isCompound()); + assert (mShapeInstance->mCollisionShape->isCompound()); - btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); + btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 636c3492f7..9b5f41a5fb 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -481,7 +481,7 @@ namespace MWPhysics if (ptr.mRef->mData.mPhysicsPostponed) return; osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); - if (!shapeInstance || !shapeInstance->getCollisionShape()) + if (!shapeInstance || !shapeInstance->mCollisionShape) return; assert(!getObject(ptr)); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index d49e6c0e8b..f9e792c7d2 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -132,7 +132,7 @@ namespace { btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 21e2d8380a..efb8b7f370 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3948,7 +3948,7 @@ namespace MWWorld btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto toLocal = object->getTransform().inverse(); const auto localFrom = toLocal(Misc::Convert::toBullet(position)); diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 6579076778..d4bdcb13b8 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -482,7 +482,7 @@ namespace osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(instance->getCollisionShape()), ObjectShapes(instance), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index 9a59ecf6d7..44b42b22c2 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -34,9 +34,9 @@ namespace DetourNavigator bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; @@ -64,9 +64,9 @@ namespace DetourNavigator bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); const CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 4b074ee146..d8315d08b6 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -77,16 +77,6 @@ BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) { } -btCollisionShape *BulletShape::getCollisionShape() const -{ - return mCollisionShape.get(); -} - -btCollisionShape *BulletShape::getAvoidCollisionShape() const -{ - return mAvoidCollisionShape.get(); -} - void BulletShape::setLocalScaling(const btVector3& scale) { mCollisionShape->setLocalScaling(scale); diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index bf249cb364..1065f3893b 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -47,10 +47,6 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - btCollisionShape* getCollisionShape() const; - - btCollisionShape* getAvoidCollisionShape() const; - void setLocalScaling(const btVector3& scale); bool isAnimated() const; From ed5a4e195b1e4bb55c89444eaa5546d3dd5edc35 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:38:38 +0200 Subject: [PATCH 054/110] Use unique_ptr to avoid possible memory leak --- components/resource/bulletshapemanager.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index ae6f3659a5..39ceb4fe7e 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -83,7 +83,8 @@ public: return osg::ref_ptr(); osg::ref_ptr shape (new BulletShape); - btBvhTriangleMeshShape* triangleMeshShape = new TriangleMeshShape(mTriangleMesh.release(), true); + + auto triangleMeshShape = std::make_unique(mTriangleMesh.release(), true); btVector3 aabbMin = triangleMeshShape->getLocalAabbMin(); btVector3 aabbMax = triangleMeshShape->getLocalAabbMax(); shape->mCollisionBox.mExtents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; @@ -92,7 +93,7 @@ public: shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, (aabbMax[1] + aabbMin[1]) / 2.0f, (aabbMax[2] + aabbMin[2]) / 2.0f ); - shape->mCollisionShape.reset(triangleMeshShape); + shape->mCollisionShape.reset(triangleMeshShape.release()); return shape; } From c83facd9d3d21a19319157edff209499062ec0fc Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:39:27 +0200 Subject: [PATCH 055/110] Avoid redundant osg::ref_ptr copy --- components/resource/bulletshape.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index d8315d08b6..1d4be1d14d 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -95,12 +95,12 @@ osg::ref_ptr makeInstance(osg::ref_ptr s } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) - : mSource(source) + : mSource(std::move(source)) { - mCollisionBox = source->mCollisionBox; - mAnimatedShapes = source->mAnimatedShapes; - mCollisionShape = duplicateCollisionShape(source->mCollisionShape.get()); - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape.get()); + mCollisionBox = mSource->mCollisionBox; + mAnimatedShapes = mSource->mAnimatedShapes; + mCollisionShape = duplicateCollisionShape(mSource->mCollisionShape.get()); + mAvoidCollisionShape = duplicateCollisionShape(mSource->mAvoidCollisionShape.get()); } } From b731a981c4267929f2e0fdf055bb1a3e1f27c6a6 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:41:19 +0200 Subject: [PATCH 056/110] Make BulletShape::isAnimated inlined --- components/resource/bulletshape.cpp | 5 ----- components/resource/bulletshape.hpp | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 1d4be1d14d..52d639d272 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -84,11 +84,6 @@ void BulletShape::setLocalScaling(const btVector3& scale) mAvoidCollisionShape->setLocalScaling(scale); } -bool BulletShape::isAnimated() const -{ - return !mAnimatedShapes.empty(); -} - osg::ref_ptr makeInstance(osg::ref_ptr source) { return {new BulletShapeInstance(std::move(source))}; diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 1065f3893b..7188165045 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -49,7 +49,7 @@ namespace Resource void setLocalScaling(const btVector3& scale); - bool isAnimated() const; + bool isAnimated() const { return !mAnimatedShapes.empty(); } }; From a851ac5fea0792f1ef6a4566d92afb16e9b971a9 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 03:46:29 +0200 Subject: [PATCH 057/110] Use custom deleter for btCompoundShape to delete children shapes --- components/nifbullet/bulletnifloader.cpp | 2 +- components/nifbullet/bulletnifloader.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 685a80951a..f808877a75 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -179,7 +179,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) child.release(); mStaticMesh.release(); } - mShape->mCollisionShape.reset(mCompoundShape.release()); + mShape->mCollisionShape = std::move(mCompoundShape); } else if (mStaticMesh) { diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 71c84566a0..3d6a95e09f 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -61,7 +61,7 @@ private: void handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); - std::unique_ptr mCompoundShape; + std::unique_ptr mCompoundShape; std::unique_ptr mStaticMesh; From f0eb068e4b99281b5d4b1843fd3135aaede7c457 Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Fri, 29 Oct 2021 18:50:04 -0700 Subject: [PATCH 058/110] remove dead code and fix changelog entry --- CHANGELOG.md | 1 - apps/openmw/mwrender/sky.hpp | 11 ----------- apps/openmw/mwrender/skyutil.cpp | 4 ---- 3 files changed, 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad6734f8c..7a486046e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4700: Editor: Incorrect command implementation Bug #4744: Invisible particles must still be processed - Bug #4752: UpdateCellCommand doesn't undo properly Bug #5088: Sky abruptly changes direction during certain weather transitions Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 227dee213b..1a30633886 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -32,17 +32,6 @@ namespace Resource namespace MWRender { - class AtmosphereUpdater; - class AtmosphereNightUpdater; - class CloudUpdater; - class Sun; - class Moon; - class RainCounter; - class RainShooter; - class RainFader; - class AlphaFader; - class UnderwaterSwitchCallback; - ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp index 9932733fa1..8229fa5925 100644 --- a/apps/openmw/mwrender/skyutil.cpp +++ b/apps/openmw/mwrender/skyutil.cpp @@ -278,10 +278,6 @@ namespace MWRender { // no traverse return; - - osg::Vec4 v4; - osg::Vec4 v3; - v4 = v3; } else { From 9c5f8b8719fbe9e89c929df46383c2a91aae43f9 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 30 Oct 2021 04:34:06 +0200 Subject: [PATCH 059/110] Store holder only in parent RecastMeshObject --- .../detournavigator/recastmeshobject.cpp | 37 +++++++------- .../detournavigator/recastmeshobject.hpp | 49 ++++++++++--------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index 8b4bc2fd6f..31aa13a208 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -11,7 +11,7 @@ namespace DetourNavigator namespace { bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children) + std::vector& children) { assert(static_cast(shape.getNumChildShapes()) == children.size()); bool result = false; @@ -23,39 +23,33 @@ namespace DetourNavigator return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCompoundShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) { - std::vector result; + std::vector result; for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - { - const CollisionShape collisionShape {holder, *shape.getChildShape(i)}; - result.emplace_back(collisionShape, shape.getChildTransform(i), areaType); - } + result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCollisionShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) { if (shape.isCompound()) - return makeChildrenObjects(holder, static_cast(shape), areaType); - return std::vector(); + return makeChildrenObjects(static_cast(shape), areaType); + return {}; } } - RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + ChildRecastMeshObject::ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) - : mHolder(shape.getHolder()) - , mShape(shape.getShape()) + : mShape(shape) , mTransform(transform) , mAreaType(areaType) - , mLocalScaling(mShape.get().getLocalScaling()) - , mChildren(makeChildrenObjects(mHolder, mShape.get(), mAreaType)) + , mLocalScaling(shape.getLocalScaling()) + , mChildren(makeChildrenObjects(shape, mAreaType)) { } - bool RecastMeshObject::update(const btTransform& transform, const AreaType areaType) + bool ChildRecastMeshObject::update(const btTransform& transform, const AreaType areaType) { bool result = false; if (!(mTransform == transform)) @@ -78,4 +72,11 @@ namespace DetourNavigator || result; return result; } + + RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + const AreaType areaType) + : mHolder(shape.getHolder()) + , mImpl(shape.getShape(), transform, areaType) + { + } } diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index 0c50c2f346..e833ee37e3 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -32,40 +32,45 @@ namespace DetourNavigator std::reference_wrapper mShape; }; + class ChildRecastMeshObject + { + public: + ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); + + bool update(const btTransform& transform, const AreaType areaType); + + const btCollisionShape& getShape() const { return mShape; } + + const btTransform& getTransform() const { return mTransform; } + + AreaType getAreaType() const { return mAreaType; } + + private: + std::reference_wrapper mShape; + btTransform mTransform; + AreaType mAreaType; + btVector3 mLocalScaling; + std::vector mChildren; + }; + class RecastMeshObject { public: RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType); - bool update(const btTransform& transform, const AreaType areaType); + bool update(const btTransform& transform, const AreaType areaType) { return mImpl.update(transform, areaType); } - const osg::ref_ptr& getHolder() const - { - return mHolder; - } + const osg::ref_ptr& getHolder() const { return mHolder; } - const btCollisionShape& getShape() const - { - return mShape; - } + const btCollisionShape& getShape() const { return mImpl.getShape(); } - const btTransform& getTransform() const - { - return mTransform; - } + const btTransform& getTransform() const { return mImpl.getTransform(); } - AreaType getAreaType() const - { - return mAreaType; - } + AreaType getAreaType() const { return mImpl.getAreaType(); } private: osg::ref_ptr mHolder; - std::reference_wrapper mShape; - btTransform mTransform; - AreaType mAreaType; - btVector3 mLocalScaling; - std::vector mChildren; + ChildRecastMeshObject mImpl; }; } From e1ac871672ea179e97b4571ec02ddd54832ca82c Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 27 Oct 2021 18:30:26 +0200 Subject: [PATCH 060/110] Start adding compiler tests --- apps/openmw_test_suite/CMakeLists.txt | 2 + .../mwscript/test_scripts.cpp | 140 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 apps/openmw_test_suite/mwscript/test_scripts.cpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 2b96d4c633..bf235331cf 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -12,6 +12,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) mwdialogue/test_keywordsearch.cpp + mwscript/test_scripts.cpp + esm/test_fixed_string.cpp esm/variant.cpp diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp new file mode 100644 index 0000000000..d019244275 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -0,0 +1,140 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return false; } + bool isJournalId(const std::string& name) const override { return false; } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; + + struct MWScriptTest : public ::testing::Test + { + MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} + + std::optional compile(const std::string& scriptBody) + { + mParser.reset(); + mErrorHandler.reset(); + std::istringstream input(scriptBody); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); + scanner.scan(mParser); + if(mErrorHandler.isGood()) + { + std::vector code; + mParser.getCode(code); + return CompiledScript(code, mParser.getLocals()); + } + return {}; + } + + void logErrors() + { + for(const auto& [error, loc] : mErrorHandler.getErrors()) + { + std::cout << error; + if(loc.mLine) + std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; + std::cout << "\n"; + } + } + + void run(const CompiledScript& script) + { + // mInterpreter.run(&script.mByteCode[0], script.mByteCode.size(), interpreterContext); + } + protected: + void SetUp() override {} + + void TearDown() override {} + private: + TestErrorHandler mErrorHandler; + TestCompilerContext mCompilerContext; + Compiler::FileParser mParser; + Interpreter::Interpreter mInterpreter; + }; + + const std::string sScript1 = R"mwscript(Begin basic_logic +; Comment +short one +short two + +set one to two + +if ( one == two ) + set one to 1 +elseif ( two == 1 ) + set one to 2 +else + set one to 3 +endif + +while ( one < two ) + set one to ( one + 1 ) +endwhile + +End)mwscript"; + + TEST_F(MWScriptTest, mwscript_test_invalid) + { + EXPECT_THROW(compile("this is not a valid script"), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_compilation) + { + auto script = compile(sScript1); + logErrors(); + EXPECT_FALSE(!script); + } +} \ No newline at end of file From 6ad8549163f543049e739607a23c3981d9fbd6cd Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 27 Oct 2021 22:00:53 +0200 Subject: [PATCH 061/110] Allow validation of constant arguments --- .../mwscript/test_scripts.cpp | 508 +++++++++++++----- 1 file changed, 369 insertions(+), 139 deletions(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index d019244275..fcb4c3de5d 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -1,140 +1,370 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -namespace -{ - class TestCompilerContext : public Compiler::Context - { - public: - bool canDeclareLocals() const override { return true; } - char getGlobalType(const std::string& name) const override { return ' '; } - std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } - bool isId(const std::string& name) const override { return false; } - bool isJournalId(const std::string& name) const override { return false; } - }; - - class TestErrorHandler : public Compiler::ErrorHandler - { - std::vector> mErrors; - - void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override - { - if(type == Compiler::ErrorHandler::ErrorMessage) - mErrors.emplace_back(message, loc); - } - - void report(const std::string& message, Compiler::ErrorHandler::Type type) override - { - report(message, {}, type); - } - - public: - void reset() override - { - Compiler::ErrorHandler::reset(); - mErrors.clear(); - } - - const std::vector>& getErrors() const { return mErrors; } - }; - - struct CompiledScript - { - std::vector mByteCode; - Compiler::Locals mLocals; - - CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} - }; - - struct MWScriptTest : public ::testing::Test - { - MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} - - std::optional compile(const std::string& scriptBody) - { - mParser.reset(); - mErrorHandler.reset(); - std::istringstream input(scriptBody); - Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); - scanner.scan(mParser); - if(mErrorHandler.isGood()) - { - std::vector code; - mParser.getCode(code); - return CompiledScript(code, mParser.getLocals()); - } - return {}; - } - - void logErrors() - { - for(const auto& [error, loc] : mErrorHandler.getErrors()) - { - std::cout << error; - if(loc.mLine) - std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; - std::cout << "\n"; - } - } - - void run(const CompiledScript& script) - { - // mInterpreter.run(&script.mByteCode[0], script.mByteCode.size(), interpreterContext); - } - protected: - void SetUp() override {} - - void TearDown() override {} - private: - TestErrorHandler mErrorHandler; - TestCompilerContext mCompilerContext; - Compiler::FileParser mParser; - Interpreter::Interpreter mInterpreter; - }; - - const std::string sScript1 = R"mwscript(Begin basic_logic -; Comment -short one -short two - -set one to two - -if ( one == two ) - set one to 1 -elseif ( two == 1 ) - set one to 2 -else - set one to 3 -endif - -while ( one < two ) - set one to ( one + 1 ) -endwhile - -End)mwscript"; - - TEST_F(MWScriptTest, mwscript_test_invalid) - { - EXPECT_THROW(compile("this is not a valid script"), Compiler::SourceException); - } - - TEST_F(MWScriptTest, mwscript_test_compilation) - { - auto script = compile(sScript1); - logErrors(); - EXPECT_FALSE(!script); - } +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return false; } + bool isJournalId(const std::string& name) const override { return false; } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + class LocalVariables + { + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + template + T getLocal(int index, const std::vector& vector) const + { + if(index < vector.size()) + return vector[index]; + return {}; + } + + template + void setLocal(T value, int index, std::vector& vector) + { + if(index >= vector.size()) + vector.resize(index + 1); + vector[index] = value; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(int index) const { return getLocal(index, mShorts); }; + + int getLong(int index) const { return getLocal(index, mLongs); }; + + float getFloat(int index) const { return getLocal(index, mFloats); }; + + void setShort(int index, int value) { setLocal(value, index, mShorts); }; + + void setLong(int index, int value) { setLocal(value, index, mLongs); }; + + void setFloat(int index, float value) { setLocal(value, index, mFloats); }; + }; + + class GlobalVariables + { + std::map mShorts; + std::map mLongs; + std::map mFloats; + + template + T getGlobal(const std::string& name, const std::map& map) const + { + auto it = map.find(name); + if(it != map.end()) + return it->second; + return {}; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(const std::string& name) const { return getGlobal(name, mShorts); }; + + int getLong(const std::string& name) const { return getGlobal(name, mLongs); }; + + float getFloat(const std::string& name) const { return getGlobal(name, mFloats); }; + + void setShort(const std::string& name, int value) { mShorts[name] = value; }; + + void setLong(const std::string& name, int value) { mLongs[name] = value; }; + + void setFloat(const std::string& name, float value) { mFloats[name] = value; }; + }; + + class TestInterpreterContext : public Interpreter::Context + { + LocalVariables mLocals; + std::map mMembers; + public: + std::string getTarget() const override { return {}; }; + + int getLocalShort(int index) const override { return mLocals.getShort(index); }; + + int getLocalLong(int index) const override { return mLocals.getLong(index); }; + + float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; + + void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; + + void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; + + void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; + + void messageBox(const std::string& message, const std::vector& buttons) override {}; + + void report(const std::string& message) override { std::cout << message << "\n"; }; + + int getGlobalShort(const std::string& name) const override { return {}; }; + + int getGlobalLong(const std::string& name) const override { return {}; }; + + float getGlobalFloat(const std::string& name) const override { return {}; }; + + void setGlobalShort(const std::string& name, int value) override {}; + + void setGlobalLong(const std::string& name, int value) override {}; + + void setGlobalFloat(const std::string& name, float value) override {}; + + std::vector getGlobals() const override { return {}; }; + + char getGlobalType(const std::string& name) const override { return ' '; }; + + std::string getActionBinding(const std::string& action) const override { return {}; }; + + std::string getActorName() const override { return {}; }; + + std::string getNPCRace() const override { return {}; }; + + std::string getNPCClass() const override { return {}; }; + + std::string getNPCFaction() const override { return {}; }; + + std::string getNPCRank() const override { return {}; }; + + std::string getPCName() const override { return {}; }; + + std::string getPCRace() const override { return {}; }; + + std::string getPCClass() const override { return {}; }; + + std::string getPCRank() const override { return {}; }; + + std::string getPCNextRank() const override { return {}; }; + + int getPCBounty() const override { return {}; }; + + std::string getCurrentCellName() const override { return {}; }; + + int getMemberShort(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getShort(name); + return {}; + }; + + int getMemberLong(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getLong(name); + return {}; + }; + + float getMemberFloat(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getFloat(name); + return {}; + }; + + void setMemberShort(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setShort(name, value); }; + + void setMemberLong(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setLong(name, value); }; + + void setMemberFloat(const std::string& id, const std::string& name, float value, bool global) override { mMembers[id].setFloat(name, value); }; + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; + + struct MWScriptTest : public ::testing::Test + { + MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} + + std::optional compile(const std::string& scriptBody, bool shouldFail = false) + { + mParser.reset(); + mErrorHandler.reset(); + std::istringstream input(scriptBody); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); + scanner.scan(mParser); + if(mErrorHandler.isGood()) + { + std::vector code; + mParser.getCode(code); + return CompiledScript(code, mParser.getLocals()); + } + else if(!shouldFail) + logErrors(); + return {}; + } + + void logErrors() + { + for(const auto& [error, loc] : mErrorHandler.getErrors()) + { + std::cout << error; + if(loc.mLine) + std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; + std::cout << "\n"; + } + } + + void registerExtensions() + { + Compiler::registerExtensions(mExtensions); + mCompilerContext.setExtensions(&mExtensions); + } + + void run(const CompiledScript& script, TestInterpreterContext& context) + { + mInterpreter.run(&script.mByteCode[0], script.mByteCode.size(), context); + } + + void installOpcode(int code, Interpreter::Opcode0* opcode) + { + mInterpreter.installSegment5(code, opcode); + } + protected: + void SetUp() override + { + Interpreter::installOpcodes(mInterpreter); + } + + void TearDown() override {} + private: + TestErrorHandler mErrorHandler; + TestCompilerContext mCompilerContext; + Compiler::FileParser mParser; + Compiler::Extensions mExtensions; + Interpreter::Interpreter mInterpreter; + }; + + const std::string sScript1 = R"mwscript(Begin basic_logic +; Comment +short one +short two + +set one to two + +if ( one == two ) + set one to 1 +elseif ( two == 1 ) + set one to 2 +else + set one to 3 +endif + +while ( one < two ) + set one to ( one + 1 ) +endwhile + +End)mwscript"; + + const std::string sScript2 = R"mwscript(Begin addtopic + +AddTopic "OpenMW Unit Test" + +End)mwscript"; + + TEST_F(MWScriptTest, mwscript_test_invalid) + { + EXPECT_THROW(compile("this is not a valid script", true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_compilation) + { + EXPECT_FALSE(!compile(sScript1)); + } + + TEST_F(MWScriptTest, mwscript_test_no_extensions) + { + EXPECT_THROW(compile(sScript2, true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_function) + { + registerExtensions(); + if(auto script = compile(sScript2)) + { + class AddTopic : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + EXPECT_EQ(topic, "OpenMW Unit Test"); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic); + TestInterpreterContext context; + run(*script, context); + } + else + { + FAIL(); + } + } } \ No newline at end of file From be759e576a08806ef93ddcec622ab280facd7a82 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 27 Oct 2021 22:08:41 +0200 Subject: [PATCH 062/110] Be sure to verify the opcode got executed --- apps/openmw_test_suite/mwscript/test_scripts.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index fcb4c3de5d..3c0cd477d5 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -346,23 +346,28 @@ End)mwscript"; TEST_F(MWScriptTest, mwscript_test_function) { registerExtensions(); + bool failed = true; if(auto script = compile(sScript2)) { class AddTopic : public Interpreter::Opcode0 { + bool& mFailed; public: + AddTopic(bool& failed) : mFailed(failed) {} + void execute(Interpreter::Runtime& runtime) { const auto topic = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); + mFailed = false; EXPECT_EQ(topic, "OpenMW Unit Test"); } }; - installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic); + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(failed)); TestInterpreterContext context; run(*script, context); } - else + if(failed) { FAIL(); } From f027acf5751b8fa75ecce9251d5dd70eef9411aa Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 27 Oct 2021 22:13:56 +0200 Subject: [PATCH 063/110] Move boilerplate to separate file --- .../mwscript/test_scripts.cpp | 237 +---------------- .../openmw_test_suite/mwscript/test_utils.hpp | 242 ++++++++++++++++++ 2 files changed, 244 insertions(+), 235 deletions(-) create mode 100644 apps/openmw_test_suite/mwscript/test_utils.hpp diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index 3c0cd477d5..90c7fb53c2 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -1,243 +1,10 @@ #include - -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include +#include "test_utils.hpp" namespace { - class TestCompilerContext : public Compiler::Context - { - public: - bool canDeclareLocals() const override { return true; } - char getGlobalType(const std::string& name) const override { return ' '; } - std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } - bool isId(const std::string& name) const override { return false; } - bool isJournalId(const std::string& name) const override { return false; } - }; - - class TestErrorHandler : public Compiler::ErrorHandler - { - std::vector> mErrors; - - void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override - { - if(type == Compiler::ErrorHandler::ErrorMessage) - mErrors.emplace_back(message, loc); - } - - void report(const std::string& message, Compiler::ErrorHandler::Type type) override - { - report(message, {}, type); - } - - public: - void reset() override - { - Compiler::ErrorHandler::reset(); - mErrors.clear(); - } - - const std::vector>& getErrors() const { return mErrors; } - }; - - class LocalVariables - { - std::vector mShorts; - std::vector mLongs; - std::vector mFloats; - - template - T getLocal(int index, const std::vector& vector) const - { - if(index < vector.size()) - return vector[index]; - return {}; - } - - template - void setLocal(T value, int index, std::vector& vector) - { - if(index >= vector.size()) - vector.resize(index + 1); - vector[index] = value; - } - public: - void clear() - { - mShorts.clear(); - mLongs.clear(); - mFloats.clear(); - } - - int getShort(int index) const { return getLocal(index, mShorts); }; - - int getLong(int index) const { return getLocal(index, mLongs); }; - - float getFloat(int index) const { return getLocal(index, mFloats); }; - - void setShort(int index, int value) { setLocal(value, index, mShorts); }; - - void setLong(int index, int value) { setLocal(value, index, mLongs); }; - - void setFloat(int index, float value) { setLocal(value, index, mFloats); }; - }; - - class GlobalVariables - { - std::map mShorts; - std::map mLongs; - std::map mFloats; - - template - T getGlobal(const std::string& name, const std::map& map) const - { - auto it = map.find(name); - if(it != map.end()) - return it->second; - return {}; - } - public: - void clear() - { - mShorts.clear(); - mLongs.clear(); - mFloats.clear(); - } - - int getShort(const std::string& name) const { return getGlobal(name, mShorts); }; - - int getLong(const std::string& name) const { return getGlobal(name, mLongs); }; - - float getFloat(const std::string& name) const { return getGlobal(name, mFloats); }; - - void setShort(const std::string& name, int value) { mShorts[name] = value; }; - - void setLong(const std::string& name, int value) { mLongs[name] = value; }; - - void setFloat(const std::string& name, float value) { mFloats[name] = value; }; - }; - - class TestInterpreterContext : public Interpreter::Context - { - LocalVariables mLocals; - std::map mMembers; - public: - std::string getTarget() const override { return {}; }; - - int getLocalShort(int index) const override { return mLocals.getShort(index); }; - - int getLocalLong(int index) const override { return mLocals.getLong(index); }; - - float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; - - void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; - - void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; - - void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; - - void messageBox(const std::string& message, const std::vector& buttons) override {}; - - void report(const std::string& message) override { std::cout << message << "\n"; }; - - int getGlobalShort(const std::string& name) const override { return {}; }; - - int getGlobalLong(const std::string& name) const override { return {}; }; - - float getGlobalFloat(const std::string& name) const override { return {}; }; - - void setGlobalShort(const std::string& name, int value) override {}; - - void setGlobalLong(const std::string& name, int value) override {}; - - void setGlobalFloat(const std::string& name, float value) override {}; - - std::vector getGlobals() const override { return {}; }; - - char getGlobalType(const std::string& name) const override { return ' '; }; - - std::string getActionBinding(const std::string& action) const override { return {}; }; - - std::string getActorName() const override { return {}; }; - - std::string getNPCRace() const override { return {}; }; - - std::string getNPCClass() const override { return {}; }; - - std::string getNPCFaction() const override { return {}; }; - - std::string getNPCRank() const override { return {}; }; - - std::string getPCName() const override { return {}; }; - - std::string getPCRace() const override { return {}; }; - - std::string getPCClass() const override { return {}; }; - - std::string getPCRank() const override { return {}; }; - - std::string getPCNextRank() const override { return {}; }; - - int getPCBounty() const override { return {}; }; - - std::string getCurrentCellName() const override { return {}; }; - - int getMemberShort(const std::string& id, const std::string& name, bool global) const override - { - auto it = mMembers.find(id); - if(it != mMembers.end()) - return it->second.getShort(name); - return {}; - }; - - int getMemberLong(const std::string& id, const std::string& name, bool global) const override - { - auto it = mMembers.find(id); - if(it != mMembers.end()) - return it->second.getLong(name); - return {}; - }; - - float getMemberFloat(const std::string& id, const std::string& name, bool global) const override - { - auto it = mMembers.find(id); - if(it != mMembers.end()) - return it->second.getFloat(name); - return {}; - }; - - void setMemberShort(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setShort(name, value); }; - - void setMemberLong(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setLong(name, value); }; - - void setMemberFloat(const std::string& id, const std::string& name, float value, bool global) override { mMembers[id].setFloat(name, value); }; - }; - - struct CompiledScript - { - std::vector mByteCode; - Compiler::Locals mLocals; - - CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} - }; - struct MWScriptTest : public ::testing::Test { MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} @@ -279,7 +46,7 @@ namespace void run(const CompiledScript& script, TestInterpreterContext& context) { - mInterpreter.run(&script.mByteCode[0], script.mByteCode.size(), context); + mInterpreter.run(&script.mByteCode[0], static_cast(script.mByteCode.size()), context); } void installOpcode(int code, Interpreter::Opcode0* opcode) diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp new file mode 100644 index 0000000000..58218b7000 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -0,0 +1,242 @@ +#ifndef MWSCRIPT_TESTING_UTIL_H +#define MWSCRIPT_TESTING_UTIL_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return false; } + bool isJournalId(const std::string& name) const override { return false; } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + class LocalVariables + { + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + template + T getLocal(int index, const std::vector& vector) const + { + if(index < vector.size()) + return vector[index]; + return {}; + } + + template + void setLocal(T value, int index, std::vector& vector) + { + if(index >= vector.size()) + vector.resize(index + 1); + vector[index] = value; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(int index) const { return getLocal(index, mShorts); }; + + int getLong(int index) const { return getLocal(index, mLongs); }; + + float getFloat(int index) const { return getLocal(index, mFloats); }; + + void setShort(int index, int value) { setLocal(value, index, mShorts); }; + + void setLong(int index, int value) { setLocal(value, index, mLongs); }; + + void setFloat(int index, float value) { setLocal(value, index, mFloats); }; + }; + + class GlobalVariables + { + std::map mShorts; + std::map mLongs; + std::map mFloats; + + template + T getGlobal(const std::string& name, const std::map& map) const + { + auto it = map.find(name); + if(it != map.end()) + return it->second; + return {}; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(const std::string& name) const { return getGlobal(name, mShorts); }; + + int getLong(const std::string& name) const { return getGlobal(name, mLongs); }; + + float getFloat(const std::string& name) const { return getGlobal(name, mFloats); }; + + void setShort(const std::string& name, int value) { mShorts[name] = value; }; + + void setLong(const std::string& name, int value) { mLongs[name] = value; }; + + void setFloat(const std::string& name, float value) { mFloats[name] = value; }; + }; + + class TestInterpreterContext : public Interpreter::Context + { + LocalVariables mLocals; + std::map mMembers; + public: + std::string getTarget() const override { return {}; }; + + int getLocalShort(int index) const override { return mLocals.getShort(index); }; + + int getLocalLong(int index) const override { return mLocals.getLong(index); }; + + float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; + + void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; + + void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; + + void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; + + void messageBox(const std::string& message, const std::vector& buttons) override {}; + + void report(const std::string& message) override {}; + + int getGlobalShort(const std::string& name) const override { return {}; }; + + int getGlobalLong(const std::string& name) const override { return {}; }; + + float getGlobalFloat(const std::string& name) const override { return {}; }; + + void setGlobalShort(const std::string& name, int value) override {}; + + void setGlobalLong(const std::string& name, int value) override {}; + + void setGlobalFloat(const std::string& name, float value) override {}; + + std::vector getGlobals() const override { return {}; }; + + char getGlobalType(const std::string& name) const override { return ' '; }; + + std::string getActionBinding(const std::string& action) const override { return {}; }; + + std::string getActorName() const override { return {}; }; + + std::string getNPCRace() const override { return {}; }; + + std::string getNPCClass() const override { return {}; }; + + std::string getNPCFaction() const override { return {}; }; + + std::string getNPCRank() const override { return {}; }; + + std::string getPCName() const override { return {}; }; + + std::string getPCRace() const override { return {}; }; + + std::string getPCClass() const override { return {}; }; + + std::string getPCRank() const override { return {}; }; + + std::string getPCNextRank() const override { return {}; }; + + int getPCBounty() const override { return {}; }; + + std::string getCurrentCellName() const override { return {}; }; + + int getMemberShort(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getShort(name); + return {}; + }; + + int getMemberLong(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getLong(name); + return {}; + }; + + float getMemberFloat(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getFloat(name); + return {}; + }; + + void setMemberShort(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setShort(name, value); }; + + void setMemberLong(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setLong(name, value); }; + + void setMemberFloat(const std::string& id, const std::string& name, float value, bool global) override { mMembers[id].setFloat(name, value); }; + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; +} + +#endif \ No newline at end of file From 3dada0796ae288f8fb6ffc31dfae003aed3f994c Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 17:34:31 +0200 Subject: [PATCH 064/110] Validate integer math --- .../mwscript/test_scripts.cpp | 60 +++++++++++++++++++ .../openmw_test_suite/mwscript/test_utils.hpp | 16 ++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index 90c7fb53c2..b2e534a401 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -93,6 +93,21 @@ End)mwscript"; AddTopic "OpenMW Unit Test" +End)mwscript"; + + const std::string sScript3 = R"mwscript(Begin math + +short a +short b +short c +short d +short e + +set b to ( a + 1 ) +set c to ( a - 1 ) +set d to ( b * c ) +set e to ( d / a ) + End)mwscript"; TEST_F(MWScriptTest, mwscript_test_invalid) @@ -139,4 +154,49 @@ End)mwscript"; FAIL(); } } + + TEST_F(MWScriptTest, mwscript_test_math) + { + if(auto script = compile(sScript3)) + { + struct Algorithm + { + int a; + int b; + int c; + int d; + int e; + + void run(int input) + { + a = input; + b = a + 1; + c = a - 1; + d = b * c; + e = d / a; + } + + void test(const TestInterpreterContext& context) const + { + EXPECT_EQ(a, context.getLocalShort(0)); + EXPECT_EQ(b, context.getLocalShort(1)); + EXPECT_EQ(c, context.getLocalShort(2)); + EXPECT_EQ(d, context.getLocalShort(3)); + EXPECT_EQ(e, context.getLocalShort(4)); + } + } algorithm; + TestInterpreterContext context; + for(int i = 1; i < 1000; ++i) + { + context.setLocalShort(0, i); + run(*script, context); + algorithm.run(i); + algorithm.test(context); + } + } + else + { + FAIL(); + } + } } \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp index 58218b7000..b1f4e04262 100644 --- a/apps/openmw_test_suite/mwscript/test_utils.hpp +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -64,7 +64,7 @@ namespace std::vector mFloats; template - T getLocal(int index, const std::vector& vector) const + T getLocal(std::size_t index, const std::vector& vector) const { if(index < vector.size()) return vector[index]; @@ -72,7 +72,7 @@ namespace } template - void setLocal(T value, int index, std::vector& vector) + void setLocal(T value, std::size_t index, std::vector& vector) { if(index >= vector.size()) vector.resize(index + 1); @@ -86,17 +86,17 @@ namespace mFloats.clear(); } - int getShort(int index) const { return getLocal(index, mShorts); }; + int getShort(std::size_t index) const { return getLocal(index, mShorts); }; - int getLong(int index) const { return getLocal(index, mLongs); }; + int getLong(std::size_t index) const { return getLocal(index, mLongs); }; - float getFloat(int index) const { return getLocal(index, mFloats); }; + float getFloat(std::size_t index) const { return getLocal(index, mFloats); }; - void setShort(int index, int value) { setLocal(value, index, mShorts); }; + void setShort(std::size_t index, int value) { setLocal(value, index, mShorts); }; - void setLong(int index, int value) { setLocal(value, index, mLongs); }; + void setLong(std::size_t index, int value) { setLocal(value, index, mLongs); }; - void setFloat(int index, float value) { setLocal(value, index, mFloats); }; + void setFloat(std::size_t index, float value) { setLocal(value, index, mFloats); }; }; class GlobalVariables From b2cdbe2e61f679bd6bfcf424a197ee2de6067990 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 18:16:00 +0200 Subject: [PATCH 065/110] Add tests for certain issues --- .../mwscript/test_scripts.cpp | 179 ++++++++++++++++++ .../openmw_test_suite/mwscript/test_utils.hpp | 2 +- 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index b2e534a401..b40fdd9b29 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -108,6 +108,98 @@ set c to ( a - 1 ) set d to ( b * c ) set e to ( d / a ) +End)mwscript"; + + const std::string sIssue3006 = R"mwscript(Begin issue3006 + +short a + +if ( a == 1 ) + set a to 2 +else set a to 3 +endif + +End)mwscript"; + + const std::string sIssue3725 = R"mwscript(Begin issue3725 + +onactivate + +if onactivate + ; do something +endif + +End)mwscript"; + + const std::string sIssue4451 = R"mwscript(Begin, GlassDisplayScript + +;[Script body] + +End, GlassDisplayScript)mwscript"; + + const std::string sIssue4597 = R"mwscript(Begin issue4597 + +short a +short b +short c +short d + +set c to 0 +set d to 0 + +if ( a <> b ) + set c to ( c + 1 ) +endif +if ( a << b ) + set c to ( c + 1 ) +endif +if ( a < b ) + set c to ( c + 1 ) +endif + +if ( a >< b ) + set d to ( d + 1 ) +endif +if ( a >> b ) + set d to ( d + 1 ) +endif +if ( a > b ) + set d to ( d + 1 ) +endif + +End)mwscript"; + + const std::string sIssue4598 = R"mwscript(Begin issue4598 + +StartScript kal_S_Pub_Jejubãr_Faraminos + +End)mwscript"; + + const std::string sIssue4867 = R"mwscript(Begin issue4867 + +float PcMagickaMult : The gameplay setting fPcBaseMagickaMult - 1.0000 + +End)mwscript"; + + const std::string sIssue4888 = R"mwscript(Begin issue4888 + +if (player->GameHour == 10) +set player->GameHour to 20 +endif + +End)mwscript"; + + const std::string sIssue5087 = R"mwscript(Begin Begin + +player->sethealth 0 +stopscript Begin + +End Begin)mwscript"; + + const std::string sIssue5097 = R"mwscript(Begin issue5097 + +setscale "0.3" + End)mwscript"; TEST_F(MWScriptTest, mwscript_test_invalid) @@ -199,4 +291,91 @@ End)mwscript"; FAIL(); } } + + TEST_F(MWScriptTest, mwscript_test_3006) + { + if(auto script = compile(sIssue3006)) + { + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 0); + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 2); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3725) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3725)); + } + + TEST_F(MWScriptTest, mwscript_test_4451) + { + EXPECT_FALSE(!compile(sIssue4451)); + } + + TEST_F(MWScriptTest, mwscript_test_4597) + { + if(auto script = compile(sIssue4597)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + if(a < b) + EXPECT_EQ(context.getLocalShort(2), 3); + else + EXPECT_EQ(context.getLocalShort(2), 0); + if(a > b) + EXPECT_EQ(context.getLocalShort(3), 3); + else + EXPECT_EQ(context.getLocalShort(3), 0); + + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4598) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue4598)); + } + + TEST_F(MWScriptTest, mwscript_test_4867) + { + EXPECT_FALSE(!compile(sIssue4867)); + } + + TEST_F(MWScriptTest, mwscript_test_4888) + { + EXPECT_FALSE(!compile(sIssue4888)); + } + + TEST_F(MWScriptTest, mwscript_test_5087) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5087)); + } + + TEST_F(MWScriptTest, mwscript_test_5097) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5097)); + } } \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp index b1f4e04262..53dfe93ad6 100644 --- a/apps/openmw_test_suite/mwscript/test_utils.hpp +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -28,7 +28,7 @@ namespace bool canDeclareLocals() const override { return true; } char getGlobalType(const std::string& name) const override { return ' '; } std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } - bool isId(const std::string& name) const override { return false; } + bool isId(const std::string& name) const override { return name == "player"; } bool isJournalId(const std::string& name) const override { return false; } }; From 8e0dfe3a8a11ec8a8221b5a818e9b5d5524e13c0 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 18:28:13 +0200 Subject: [PATCH 066/110] Add tests for more issues --- .../mwscript/test_scripts.cpp | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index b40fdd9b29..258eef3ae7 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -129,6 +129,10 @@ if onactivate ; do something endif +End)mwscript"; + + const std::string sIssue4061 = R"mwscript(Begin 01_Rz_neuvazhay-koryto2 + End)mwscript"; const std::string sIssue4451 = R"mwscript(Begin, GlassDisplayScript @@ -173,6 +177,12 @@ End)mwscript"; StartScript kal_S_Pub_Jejubãr_Faraminos +End)mwscript"; + + const std::string sIssue4803 = R"mwscript( +-- ++-Begin issue4803 + End)mwscript"; const std::string sIssue4867 = R"mwscript(Begin issue4867 @@ -200,6 +210,21 @@ End Begin)mwscript"; setscale "0.3" +End)mwscript"; + + const std::string sIssue5345 = R"mwscript(Begin issue5345 + +StartScript DN_MinionDrain_s" + +End)mwscript"; + + const std::string sIssue6066 = R"mwscript(Begin issue6066 +addtopic "return" + +End)mwscript"; + + const std::string sIssue6282 = R"mwscript(Begin 11AA_LauraScript7.5 + End)mwscript"; TEST_F(MWScriptTest, mwscript_test_invalid) @@ -316,6 +341,11 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue3725)); } + TEST_F(MWScriptTest, mwscript_test_4061) + { + EXPECT_FALSE(!compile(sIssue4061)); + } + TEST_F(MWScriptTest, mwscript_test_4451) { EXPECT_FALSE(!compile(sIssue4451)); @@ -357,6 +387,11 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue4598)); } + TEST_F(MWScriptTest, mwscript_test_4803) + { + EXPECT_FALSE(!compile(sIssue4803)); + } + TEST_F(MWScriptTest, mwscript_test_4867) { EXPECT_FALSE(!compile(sIssue4867)); @@ -378,4 +413,21 @@ End)mwscript"; registerExtensions(); EXPECT_FALSE(!compile(sIssue5097)); } + + TEST_F(MWScriptTest, mwscript_test_5345) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5345)); + } + + TEST_F(MWScriptTest, mwscript_test_6066) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue6066)); + } + + TEST_F(MWScriptTest, mwscript_test_6282) + { + EXPECT_FALSE(!compile(sIssue6282)); + } } \ No newline at end of file From b3208f4066b63acbfe30b40be4ab3b9f88ce669c Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 21:44:08 +0200 Subject: [PATCH 067/110] Add more issues and a forum thread --- .../mwscript/test_scripts.cpp | 90 +++++++++++++++++++ .../openmw_test_suite/mwscript/test_utils.hpp | 4 +- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index 258eef3ae7..c7b7af6163 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -108,6 +108,33 @@ set c to ( a - 1 ) set d to ( b * c ) set e to ( d / a ) +End)mwscript"; + +// https://forum.openmw.org/viewtopic.php?f=6&t=2262 + const std::string sScript4 = R"mwscript(Begin scripting_once_again + +player -> addSpell "fire_bite", 645 + +PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 + +End)mwscript"; + + const std::string sIssue1430 = R"mwscript(Begin issue1430 + +short var +If ( menumode == 1 ) + Player->AddItem "fur_boots", 1 + Player->Equip "iron battle axe", 1 + player->addspell "fire bite", 645 + player->additem "ring_keley", 1, +endif + +End)mwscript"; + + const std::string sIssue1767 = R"mwscript(Begin issue1767 + +player->GetPcRank "temple" + End)mwscript"; const std::string sIssue3006 = R"mwscript(Begin issue3006 @@ -225,6 +252,18 @@ End)mwscript"; const std::string sIssue6282 = R"mwscript(Begin 11AA_LauraScript7.5 +End)mwscript"; + + const std::string sIssue6363 = R"mwscript(Begin issue6363 + +short 1 + +if ( "1" == 1 ) + PositionCell 0 1 2 3 4 5 "Morrowland" +endif + +set 1 to 42 + End)mwscript"; TEST_F(MWScriptTest, mwscript_test_invalid) @@ -317,6 +356,24 @@ End)mwscript"; } } + TEST_F(MWScriptTest, mwscript_test_forum_thread) + { + registerExtensions(); + EXPECT_FALSE(!compile(sScript4)); + } + + TEST_F(MWScriptTest, mwscript_test_1430) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1430)); + } + + TEST_F(MWScriptTest, mwscript_test_1767) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1767)); + } + TEST_F(MWScriptTest, mwscript_test_3006) { if(auto script = compile(sIssue3006)) @@ -430,4 +487,37 @@ End)mwscript"; { EXPECT_FALSE(!compile(sIssue6282)); } + + TEST_F(MWScriptTest, mwscript_test_6363) + { + registerExtensions(); + if(const auto script = compile(sIssue6363)) + { + class PositionCell : public Interpreter::Opcode0 + { + bool& mRan; + public: + PositionCell(bool& ran) : mRan(ran) {} + + void execute(Interpreter::Runtime& runtime) + { + mRan = true; + } + }; + bool ran = false; + installOpcode(Compiler::Transformation::opcodePositionCell, new PositionCell(ran)); + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_FALSE(ran); + ran = false; + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_TRUE(ran); + } + else + { + FAIL(); + } + } } \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp index 53dfe93ad6..1d0f39d2b0 100644 --- a/apps/openmw_test_suite/mwscript/test_utils.hpp +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -20,6 +20,8 @@ #include #include +#include + namespace { class TestCompilerContext : public Compiler::Context @@ -28,7 +30,7 @@ namespace bool canDeclareLocals() const override { return true; } char getGlobalType(const std::string& name) const override { return ' '; } std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } - bool isId(const std::string& name) const override { return name == "player"; } + bool isId(const std::string& name) const override { return Misc::StringUtils::ciEqual(name, "player"); } bool isJournalId(const std::string& name) const override { return false; } }; From 319d30fb85992cbd6610028fba5f587604f0a446 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 28 Oct 2021 22:02:58 +0200 Subject: [PATCH 068/110] Add AddTopic testing --- apps/openmw_test_suite/mwscript/test_scripts.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index c7b7af6163..f6ffe0def8 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -156,6 +156,13 @@ if onactivate ; do something endif +End)mwscript"; + + const std::string sIssue3846 = R"mwscript(Begin issue3846 + +Addtopic -spells... +Addtopic -magicka... + End)mwscript"; const std::string sIssue4061 = R"mwscript(Begin 01_Rz_neuvazhay-koryto2 @@ -398,6 +405,12 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue3725)); } + TEST_F(MWScriptTest, mwscript_test_3846) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3846)); + } + TEST_F(MWScriptTest, mwscript_test_4061) { EXPECT_FALSE(!compile(sIssue4061)); From 3c5a50cf904d1a1a9541b4daa423a23a28f4636e Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 30 Oct 2021 12:09:02 +0200 Subject: [PATCH 069/110] Add issues from Redmine --- .../mwscript/test_scripts.cpp | 202 +++++++++++++++++- .../openmw_test_suite/mwscript/test_utils.hpp | 1 - 2 files changed, 197 insertions(+), 6 deletions(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index f6ffe0def8..0facafbe87 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -117,6 +117,36 @@ player -> addSpell "fire_bite", 645 PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 +End)mwscript"; + + const std::string sIssue587 = R"mwscript(Begin stalresetScript + +End stalreset Script)mwscript"; + + const std::string sIssue677 = R"mwscript(Begin _ase_dtree_dtree-owls + +End)mwscript"; + + const std::string sIssue685 = R"mwscript(Begin issue685 + +Choice: "Sicher. Hier, nehmt." 1 "Nein, ich denke nicht. Tut mir Leid." 2 +StartScript GetPCGold + +End)mwscript"; + + const std::string sIssue694 = R"mwscript(Begin issue694 + +float timer + +if ( timer < .1 ) +endif + +End)mwscript"; + + const std::string sIssue1062 = R"mwscript(Begin issue1026 + +short end + End)mwscript"; const std::string sIssue1430 = R"mwscript(Begin issue1430 @@ -129,12 +159,58 @@ If ( menumode == 1 ) player->additem "ring_keley", 1, endif +End)mwscript"; + + const std::string sIssue1593 = R"mwscript(Begin changeWater_-550_400 + +End)mwscript"; + + const std::string sIssue1730 = R"mwscript(Begin 4LOM_Corprusarium_Guards + End)mwscript"; const std::string sIssue1767 = R"mwscript(Begin issue1767 player->GetPcRank "temple" +End)mwscript"; + + const std::string sIssue2206 = R"mwscript(Begin issue2206 + +Choice ."Sklavin kaufen." 1 "Lebt wohl." 2 +Choice Choice "Insister pour qu’il vous réponde." 6 "Le prier de vous accorder un peu de son temps." 6 " Le menacer de révéler qu'il prélève sa part sur les bénéfices de la mine d’ébonite." 7 + +End)mwscript"; + + const std::string sIssue2207 = R"mwscript(Begin issue2207 + +PositionCell -35 –473 -248 0 "Skaal-Dorf, Die Große Halle" + +End)mwscript"; + + const std::string sIssue2794 = R"mwscript(Begin issue2794 + +if ( player->"getlevel" == 1 ) + ; do something +endif + +End)mwscript"; + + const std::string sIssue2830 = R"mwscript(Begin issue2830 + +AddItem "if" 1 +AddItem "endif" 1 +GetItemCount "begin" + +End)mwscript"; + + const std::string sIssue2991 = R"mwscript(Begin issue2991 + +MessageBox "OnActivate" +messagebox "messagebox" +messagebox "if" +messagebox "tcl" + End)mwscript"; const std::string sIssue3006 = R"mwscript(Begin issue3006 @@ -156,6 +232,18 @@ if onactivate ; do something endif +End)mwscript"; + + const std::string sIssue3836 = R"mwscript(Begin issue3836 + +MessageBox " Membership Level: %.0f +Account Balance: %.0f +Your Gold: %.0f +Interest Rate: %.3f +Service Charge Rate: %.3f +Total Service Charges: %.0f +Total Interest Earned: %.0f " Membership BankAccount YourGold InterestRate ServiceRate TotalServiceCharges TotalInterestEarned + End)mwscript"; const std::string sIssue3846 = R"mwscript(Begin issue3846 @@ -292,7 +380,7 @@ End)mwscript"; { registerExtensions(); bool failed = true; - if(auto script = compile(sScript2)) + if(const auto script = compile(sScript2)) { class AddTopic : public Interpreter::Opcode0 { @@ -320,7 +408,7 @@ End)mwscript"; TEST_F(MWScriptTest, mwscript_test_math) { - if(auto script = compile(sScript3)) + if(const auto script = compile(sScript3)) { struct Algorithm { @@ -369,21 +457,94 @@ End)mwscript"; EXPECT_FALSE(!compile(sScript4)); } + TEST_F(MWScriptTest, mwscript_test_587) + { + EXPECT_FALSE(!compile(sIssue587)); + } + + TEST_F(MWScriptTest, mwscript_test_677) + { + EXPECT_FALSE(!compile(sIssue677)); + } + + TEST_F(MWScriptTest, mwscript_test_685) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue685)); + } + + TEST_F(MWScriptTest, mwscript_test_694) + { + EXPECT_FALSE(!compile(sIssue694)); + } + + TEST_F(MWScriptTest, mwscript_test_1062) + { + if(const auto script = compile(sIssue1062)) + { + EXPECT_EQ(script->mLocals.getIndex("end"), 0); + } + else + { + FAIL(); + } + } + TEST_F(MWScriptTest, mwscript_test_1430) { registerExtensions(); EXPECT_FALSE(!compile(sIssue1430)); } + TEST_F(MWScriptTest, mwscript_test_1593) + { + EXPECT_FALSE(!compile(sIssue1593)); + } + + TEST_F(MWScriptTest, mwscript_test_1730) + { + EXPECT_FALSE(!compile(sIssue1730)); + } + TEST_F(MWScriptTest, mwscript_test_1767) { registerExtensions(); EXPECT_FALSE(!compile(sIssue1767)); } + TEST_F(MWScriptTest, mwscript_test_2206) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2206)); + } + + TEST_F(MWScriptTest, mwscript_test_2207) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2207)); + } + + TEST_F(MWScriptTest, mwscript_test_2794) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2794)); + } + + TEST_F(MWScriptTest, mwscript_test_2830) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2830)); + } + + TEST_F(MWScriptTest, mwscript_test_2991) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2991)); + } + TEST_F(MWScriptTest, mwscript_test_3006) { - if(auto script = compile(sIssue3006)) + if(const auto script = compile(sIssue3006)) { TestInterpreterContext context; context.setLocalShort(0, 0); @@ -405,10 +566,41 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue3725)); } + TEST_F(MWScriptTest, mwscript_test_3836) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3836)); + } + TEST_F(MWScriptTest, mwscript_test_3846) { registerExtensions(); - EXPECT_FALSE(!compile(sIssue3846)); + if(const auto script = compile(sIssue3846)) + { + std::vector topics = { "-spells...", "-magicka..." }; + class AddTopic : public Interpreter::Opcode0 + { + std::vector& mTopics; + public: + AddTopic(std::vector& topics) : mTopics(topics) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + EXPECT_EQ(topic, mTopics[0]); + mTopics.erase(mTopics.begin()); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(topics)); + TestInterpreterContext context; + run(*script, context); + EXPECT_TRUE(topics.empty()); + } + else + { + FAIL(); + } } TEST_F(MWScriptTest, mwscript_test_4061) @@ -423,7 +615,7 @@ End)mwscript"; TEST_F(MWScriptTest, mwscript_test_4597) { - if(auto script = compile(sIssue4597)) + if(const auto script = compile(sIssue4597)) { TestInterpreterContext context; for(int a = 0; a < 100; ++a) diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp index 1d0f39d2b0..f29cb7bb89 100644 --- a/apps/openmw_test_suite/mwscript/test_utils.hpp +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -31,7 +31,6 @@ namespace char getGlobalType(const std::string& name) const override { return ' '; } std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } bool isId(const std::string& name) const override { return Misc::StringUtils::ciEqual(name, "player"); } - bool isJournalId(const std::string& name) const override { return false; } }; class TestErrorHandler : public Compiler::ErrorHandler From ae08f942d5142ff1c3e05f0a3a326732e87021a0 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 30 Oct 2021 12:23:14 +0200 Subject: [PATCH 070/110] Test binary operators --- .../mwscript/test_scripts.cpp | 124 ++++++++++++++++-- 1 file changed, 115 insertions(+), 9 deletions(-) diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index 0facafbe87..79db3bb414 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -173,6 +173,49 @@ End)mwscript"; player->GetPcRank "temple" +End)mwscript"; + + const std::string sIssue2185 = R"mwscript(Begin issue2185 + +short a +short b +short eq +short gte +short lte +short ne + +set eq to 0 +if ( a == b ) + set eq to ( eq + 1 ) +endif +if ( a = = b ) + set eq to ( eq + 1 ) +endif + +set gte to 0 +if ( a >= b ) + set gte to ( gte + 1 ) +endif +if ( a > = b ) + set gte to ( gte + 1 ) +endif + +set lte to 0 +if ( a <= b ) + set lte to ( lte + 1 ) +endif +if ( a < = b ) + set lte to ( lte + 1 ) +endif + +set ne to 0 +if ( a != b ) + set ne to ( ne + 1 ) +endif +if ( a ! = b ) + set ne to ( ne + 1 ) +endif + End)mwscript"; const std::string sIssue2206 = R"mwscript(Begin issue2206 @@ -232,6 +275,29 @@ if onactivate ; do something endif +End)mwscript"; + + const std::string sIssue3744 = R"mwscript(Begin issue3744 + +short a +short b +short c + +set c to 0 + +if ( a => b ) + set c to ( c + 1 ) +endif +if ( a =< b ) + set c to ( c + 1 ) +endif +if ( a = b ) + set c to ( c + 1 ) +endif +if ( a == b ) + set c to ( c + 1 ) +endif + End)mwscript"; const std::string sIssue3836 = R"mwscript(Begin issue3836 @@ -512,6 +578,31 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue1767)); } + TEST_F(MWScriptTest, mwscript_test_2185) + { + if(const auto script = compile(sIssue2185)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(3), a >= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(4), a <= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(5), a != b ? 2 : 0); + } + } + } + else + { + FAIL(); + } + } + TEST_F(MWScriptTest, mwscript_test_2206) { registerExtensions(); @@ -566,6 +657,28 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue3725)); } + TEST_F(MWScriptTest, mwscript_test_3744) + { + if(const auto script = compile(sIssue3744)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 4 : 0); + } + } + } + else + { + FAIL(); + } + } + TEST_F(MWScriptTest, mwscript_test_3836) { registerExtensions(); @@ -625,15 +738,8 @@ End)mwscript"; context.setLocalShort(0, a); context.setLocalShort(1, b); run(*script, context); - if(a < b) - EXPECT_EQ(context.getLocalShort(2), 3); - else - EXPECT_EQ(context.getLocalShort(2), 0); - if(a > b) - EXPECT_EQ(context.getLocalShort(3), 3); - else - EXPECT_EQ(context.getLocalShort(3), 0); - + EXPECT_EQ(context.getLocalShort(2), a < b ? 3 : 0); + EXPECT_EQ(context.getLocalShort(3), a > b ? 3 : 0); } } } From 7e6533f0f3d793e74ed38148ececa51397cc75e9 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 30 Oct 2021 17:10:16 +0000 Subject: [PATCH 071/110] refactors screenshot360 (#3202) With this PR we refactor screenshot360 not to manually adjust node masks. --- apps/openmw/mwrender/renderingmanager.cpp | 7 ------- apps/openmw/mwrender/screenshotmanager.cpp | 8 ++------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 7b8393f690..c4ef1d9d9b 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -902,15 +902,8 @@ namespace MWRender return false; } - unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); - - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(0); - mScreenshotManager->screenshot360(image); - mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); - return true; } diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index 5a047a1566..ab7d0d93f0 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -251,14 +251,13 @@ namespace MWRender osg::ref_ptr screenshotCamera(new osg::Camera); osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0), 2.0))); - quad->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); std::map defineMap; Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr stateset = quad->getOrCreateStateSet(); osg::ref_ptr program(new osg::Program); program->addShader(fragmentShader); @@ -269,9 +268,6 @@ namespace MWRender stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); - quad->setStateSet(stateset); - quad->setUpdateCallback(nullptr); - screenshotCamera->addChild(quad); renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); @@ -347,7 +343,7 @@ namespace MWRender rttCamera->addChild(mWater->getReflectionNode()); rttCamera->addChild(mWater->getRefractionNode()); - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); + rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & ~(Mask_GUI|Mask_FirstPerson)); rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); From 7d34149adc56f2d924795200c3b5b408fff56517 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 30 Oct 2021 17:19:58 +0000 Subject: [PATCH 072/110] consolidates createUnlitMaterial (#3203) With this PR we remove a few duplicate lines of code by adding a parameter to `createUnlitMaterial`. --- apps/openmw/mwrender/skyutil.cpp | 24 +++++++++--------------- apps/openmw/mwrender/skyutil.hpp | 2 +- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp index 8229fa5925..0e2955333f 100644 --- a/apps/openmw/mwrender/skyutil.cpp +++ b/apps/openmw/mwrender/skyutil.cpp @@ -93,26 +93,20 @@ namespace namespace MWRender { - osg::ref_ptr createAlphaTrackingUnlitMaterial() + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode) { osg::ref_ptr mat = new osg::Material; mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); + mat->setColorMode(colorMode); return mat; } - osg::ref_ptr createUnlitMaterial() + osg::ref_ptr createAlphaTrackingUnlitMaterial() { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; + return createUnlitMaterial(osg::Material::DIFFUSE); } class SunUpdater : public SceneUtil::StateSetUpdater @@ -126,7 +120,7 @@ namespace MWRender void setDefaults(osg::StateSet* stateset) override { - stateset->setAttributeAndModes(MWRender::createUnlitMaterial()); + stateset->setAttributeAndModes(createUnlitMaterial()); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override @@ -188,7 +182,7 @@ namespace MWRender if (visibleRatio < fadeThreshold) { float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (MWRender::createUnlitMaterial()); + osg::ref_ptr mat (createUnlitMaterial()); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); stateset = new osg::StateSet; stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); @@ -283,7 +277,7 @@ namespace MWRender { osg::ref_ptr stateset = new osg::StateSet; - osg::ref_ptr mat = MWRender::createUnlitMaterial(); + osg::ref_ptr mat = createUnlitMaterial(); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); @@ -364,7 +358,7 @@ namespace MWRender stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); stateset->addUniform(new osg::Uniform("diffuseMap", 0)); stateset->addUniform(new osg::Uniform("maskMap", 1)); - stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } else { @@ -386,7 +380,7 @@ namespace MWRender texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency stateset->setTextureAttributeAndModes(1, texEnv2); - stateset->setAttributeAndModes(MWRender::createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } } diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp index ae04c88d23..c2272143a0 100644 --- a/apps/openmw/mwrender/skyutil.hpp +++ b/apps/openmw/mwrender/skyutil.hpp @@ -104,7 +104,7 @@ namespace MWRender }; osg::ref_ptr createAlphaTrackingUnlitMaterial(); - osg::ref_ptr createUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode = osg::Material::OFF); class OcclusionCallback { From 1329f22186d8c2de2d17be57a87ce4c020cbbdeb Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 30 Oct 2021 17:27:57 +0000 Subject: [PATCH 073/110] refactors MWWorld::Store maps (#3197) With this PR we clean up `MWWorld::Store` maps according to the approach of PR #3184. --- apps/openmw/mwworld/esmstore.cpp | 13 ++-- apps/openmw/mwworld/esmstore.hpp | 14 ++-- apps/openmw/mwworld/store.cpp | 121 ++++++++++--------------------- apps/openmw/mwworld/store.hpp | 16 ++-- 4 files changed, 57 insertions(+), 107 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 66aba9139e..3cedcd457a 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -61,7 +61,7 @@ namespace } } - std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) + std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found std::string defaultCls; @@ -114,8 +114,8 @@ namespace // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no longer exists however. // So instead of removing the item altogether, we're only removing the script. - template - void removeMissingScripts(const MWWorld::Store& scripts, std::map& items) + template + void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) { for(auto& [id, item] : items) { @@ -324,7 +324,6 @@ void ESMStore::countRecords() if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; - Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } }; @@ -333,8 +332,7 @@ void ESMStore::countRecords() int ESMStore::getRefCount(const std::string& id) const { - const std::string lowerId = Misc::StringUtils::lowerCase(id); - auto it = mRefCount.find(lowerId); + auto it = mRefCount.find(id); if(it == mRefCount.end()) return 0; return it->second; @@ -533,9 +531,8 @@ void ESMStore::removeMissingObjects(Store& store) throw std::runtime_error ("Invalid player record (race or class unavailable"); } - std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const + std::pair, bool> ESMStore::getSpellList(const std::string& id) const { - const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 22b58f77dd..d1a4942cd3 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -75,16 +75,17 @@ namespace MWWorld // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. - std::map mIds; - std::map mStaticIds; + using IDMap = std::unordered_map; + IDMap mIds; + IDMap mStaticIds; - std::unordered_map mRefCount; + IDMap mRefCount; std::map mStores; unsigned int mDynamicCount; - mutable std::map > mSpellListCache; + mutable std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> mSpellListCache; /// Validate entries in store after setup void validate(); @@ -115,10 +116,9 @@ namespace MWWorld } /// Look up the given ID in 'all'. Returns 0 if not found. - /// \note id must be in lower case. int find(const std::string &id) const { - std::map::const_iterator it = mIds.find(id); + IDMap::const_iterator it = mIds.find(id); if (it == mIds.end()) { return 0; } @@ -126,7 +126,7 @@ namespace MWWorld } int findStatic(const std::string &id) const { - std::map::const_iterator it = mStaticIds.find(id); + IDMap::const_iterator it = mStaticIds.find(id); if (it == mStaticIds.end()) { return 0; } diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 963e3b78cd..32458a2e84 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -38,8 +38,8 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - - mStatic.insert_or_assign(record.mIndex, record); + auto idx = record.mIndex; + mStatic.insert_or_assign(idx, std::move(record)); } template int IndexedStore::getSize() const @@ -98,13 +98,11 @@ namespace MWWorld template const T *Store::search(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename Dynamic::const_iterator dit = mDynamic.find(idLower); + typename Dynamic::const_iterator dit = mDynamic.find(id); if (dit != mDynamic.end()) return &dit->second; - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -113,8 +111,7 @@ namespace MWWorld template const T *Store::searchStatic(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -159,7 +156,7 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - Misc::StringUtils::lowerCaseInPlace(record.mId); + Misc::StringUtils::lowerCaseInPlace(record.mId); // TODO: remove this line once we have ported our remaining code base to lowercase on lookup std::pair inserted = mStatic.insert_or_assign(record.mId, record); if (inserted.second) @@ -206,14 +203,13 @@ namespace MWWorld template T *Store::insert(const T &item, bool overrideOnly) { - std::string id = Misc::StringUtils::lowerCase(item.mId); if(overrideOnly) { - auto it = mStatic.find(id); + auto it = mStatic.find(item.mId); if(it == mStatic.end()) return nullptr; } - std::pair result = mDynamic.insert_or_assign(id, item); + std::pair result = mDynamic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -222,8 +218,7 @@ namespace MWWorld template T *Store::insertStatic(const T &item) { - std::string id = Misc::StringUtils::lowerCase(item.mId); - std::pair result = mStatic.insert_or_assign(id, item); + std::pair result = mStatic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -232,9 +227,7 @@ namespace MWWorld template bool Store::eraseStatic(const std::string &id) { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename std::map::iterator it = mStatic.find(idLower); + typename Static::iterator it = mStatic.find(id); if (it != mStatic.end()) { // delete from the static part of mShared @@ -242,7 +235,7 @@ namespace MWWorld typename std::vector::iterator end = sharedIter + mStatic.size(); while (sharedIter != mShared.end() && sharedIter != end) { - if((*sharedIter)->mId == idLower) { + if(Misc::StringUtils::ciEqual((*sharedIter)->mId, id)) { mShared.erase(sharedIter); break; } @@ -257,17 +250,13 @@ namespace MWWorld template bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - typename Dynamic::iterator it = mDynamic.find(key); - if (it == mDynamic.end()) { + if (!mDynamic.erase(id)) return false; - } - mDynamic.erase(it); // have to reinit the whole shared part assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); - for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { + for (auto it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); } return true; @@ -353,12 +342,8 @@ namespace MWWorld ESM::LandTexture* tex = const_cast(search(lt.mIndex, i)); if (tex) { - const std::string texId = Misc::StringUtils::lowerCase(tex->mId); - const std::string ltId = Misc::StringUtils::lowerCase(lt.mId); - if (texId == ltId) - { + if (Misc::StringUtils::ciEqual(tex->mId, lt.mId)) tex->mTexture = lt.mTexture; - } } } @@ -367,9 +352,10 @@ namespace MWWorld ltexl.resize(lt.mIndex+1); // Store it - ltexl[lt.mIndex] = lt; + auto idx = lt.mIndex; + ltexl[idx] = std::move(lt); - return RecordId(lt.mId, isDeleted); + return RecordId(ltexl[idx].mId, isDeleted); } RecordId Store::load(ESM::ESMReader &esm) { @@ -503,16 +489,12 @@ namespace MWWorld } const ESM::Cell *Store::search(const std::string &id) const { - ESM::Cell cell; - cell.mName = Misc::StringUtils::lowerCase(id); - - std::map::const_iterator it = mInt.find(cell.mName); - + DynamicInt::const_iterator it = mInt.find(id); if (it != mInt.end()) { return &(it->second); } - DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); + DynamicInt::const_iterator dit = mDynamicInt.find(id); if (dit != mDynamicInt.end()) { return &dit->second; } @@ -521,48 +503,34 @@ namespace MWWorld } const ESM::Cell *Store::search(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } return nullptr; } const ESM::Cell *Store::searchStatic(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - - std::pair key(x, y); - DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + DynamicExt::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) return &(it->second); - } return nullptr; } const ESM::Cell *Store::searchOrCreate(int x, int y) { std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } ESM::Cell newCell; newCell.mData.mX = x; @@ -625,12 +593,11 @@ namespace MWWorld // Load the (x,y) coordinates of the cell, if it is an exterior cell, // so we can find the cell we need to merge with cell.loadNameAndData(esm, isDeleted); - std::string idLower = Misc::StringUtils::lowerCase(cell.mName); if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. - ESM::Cell *oldcell = const_cast(search(idLower)); + ESM::Cell *oldcell = const_cast(search(cell.mName)); if (oldcell) { // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) @@ -642,7 +609,7 @@ namespace MWWorld // spawn a new cell cell.loadCell(esm, true); - mInt[idLower] = cell; + mInt[cell.mName] = cell; } } else @@ -780,27 +747,19 @@ namespace MWWorld const std::string cellType = (cell.isExterior()) ? "exterior" : "interior"; throw std::runtime_error("Failed to create " + cellType + " cell"); } - ESM::Cell *ptr; if (cell.isExterior()) { std::pair key(cell.getGridX(), cell.getGridY()); // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicExt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedExt.push_back(ptr); + DynamicExt::iterator result = mDynamicExt.emplace(key, cell).first; + mSharedExt.push_back(&result->second); + return &result->second; } else { - std::string key = Misc::StringUtils::lowerCase(cell.mName); - // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicInt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedInt.push_back(ptr); + DynamicInt::iterator result = mDynamicInt.emplace(cell.mName, cell).first; + mSharedInt.push_back(&result->second); + return &result->second; } - return ptr; } bool Store::erase(const ESM::Cell &cell) { @@ -811,8 +770,7 @@ namespace MWWorld } bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - DynamicInt::iterator it = mDynamicInt.find(key); + DynamicInt::iterator it = mDynamicInt.find(id); if (it == mDynamicInt.end()) { return false; @@ -1062,12 +1020,11 @@ namespace MWWorld dialogue.loadId(esm); - std::string idLower = Misc::StringUtils::lowerCase(dialogue.mId); - std::map::iterator found = mStatic.find(idLower); + Static::iterator found = mStatic.find(dialogue.mId); if (found == mStatic.end()) { dialogue.loadData(esm, isDeleted); - mStatic.insert(std::make_pair(idLower, dialogue)); + mStatic.emplace(dialogue.mId, dialogue); } else { @@ -1081,11 +1038,7 @@ namespace MWWorld template<> bool Store::eraseStatic(const std::string &id) { - auto it = mStatic.find(Misc::StringUtils::lowerCase(id)); - - if (it != mStatic.end()) - mStatic.erase(it); - + mStatic.erase(id); return true; } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 4b1d648703..dbb28258ef 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "recordcmp.hpp" @@ -147,14 +148,13 @@ namespace MWWorld template class Store : public StoreBase { - std::map mStatic; - std::vector mShared; // Preserves the record order as it came from the content files (this + typedef std::unordered_map Static; + Static mStatic; + std::vector mShared; // Preserves the record order as it came from the content files (this // is relevant for the spell autocalc code and selection order // for heads/hairs in the character creation) - std::map mDynamic; - - typedef std::map Dynamic; - typedef std::map Static; + typedef std::unordered_map Dynamic; + Dynamic mDynamic; friend class ESMStore; @@ -294,7 +294,7 @@ namespace MWWorld } }; - typedef std::map DynamicInt; + typedef std::unordered_map DynamicInt; typedef std::map, ESM::Cell, DynamicExtCmp> DynamicExt; DynamicInt mInt; @@ -354,7 +354,7 @@ namespace MWWorld class Store : public StoreBase { private: - typedef std::map Interior; + typedef std::unordered_map Interior; typedef std::map, ESM::Pathgrid> Exterior; Interior mInt; From 356e9d7cf0a6313417515589790f1873cce45054 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sat, 30 Oct 2021 20:43:18 +0000 Subject: [PATCH 074/110] refactors osg::Callback virtual inheritance (#3200) With this PR we refactor `SceneUtil::KeyframeController` not to require `virtual osg::Callback` inheritance. I suppose such `virtual` overhead is not justified here because it negatively impacts many other classes we derive from `osg::Callback`. --- apps/openmw/mwrender/animation.cpp | 5 +++-- components/nifosg/controller.hpp | 1 + components/sceneutil/keyframe.hpp | 14 ++++++++------ components/sceneutil/nodecallback.hpp | 2 +- components/sceneutil/osgacontroller.hpp | 2 ++ 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index cdc6c49ab2..91ef6cc975 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -968,8 +968,9 @@ namespace MWRender { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource - node->addUpdateCallback(it->second); - mActiveControllers.emplace_back(node, it->second); + osg::Callback* callback = it->second->getAsCallback(); + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); if (blendMask == 0 && node == mAccumRoot) { diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index c6311fd5fc..5d88dda1f1 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -248,6 +248,7 @@ namespace NifOsg META_Object(NifOsg, KeyframeController) osg::Vec3f getTranslation(float time) const override; + osg::Callback* getAsCallback() override { return this; } void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp index 5be6924a09..59a87ab08e 100644 --- a/components/sceneutil/keyframe.hpp +++ b/components/sceneutil/keyframe.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include #include @@ -11,18 +11,20 @@ namespace SceneUtil { - class KeyframeController : public SceneUtil::Controller, public virtual osg::Callback + /// @note Derived classes are expected to derive from osg::Callback and implement getAsCallback(). + class KeyframeController : public SceneUtil::Controller, public virtual osg::Object { public: KeyframeController() {} KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop) - : osg::Callback(copy, copyop) - , SceneUtil::Controller(copy) - {} - META_Object(SceneUtil, KeyframeController) + : osg::Object(copy, copyop) + , SceneUtil::Controller(copy) {} virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } + + /// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on. + virtual osg::Callback* getAsCallback() = 0; }; /// Wrapper object containing an animation track as a ref-countable osg::Object. diff --git a/components/sceneutil/nodecallback.hpp b/components/sceneutil/nodecallback.hpp index 6f0140d64c..942cb17ded 100644 --- a/components/sceneutil/nodecallback.hpp +++ b/components/sceneutil/nodecallback.hpp @@ -13,7 +13,7 @@ namespace SceneUtil { template -class NodeCallback : public virtual osg::Callback +class NodeCallback : public osg::Callback { public: NodeCallback(){} diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp index 26212a3b99..893b8b1ebe 100644 --- a/components/sceneutil/osgacontroller.hpp +++ b/components/sceneutil/osgacontroller.hpp @@ -45,6 +45,8 @@ namespace SceneUtil META_Object(SceneUtil, OsgAnimationController) + osg::Callback* getAsCallback() override { return this; } + /// @brief Handles the location of the instance osg::Vec3f getTranslation(float time) const override; From 3c40935ec16cde3d5db8ce2fe26da63fb2d07c17 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 31 Oct 2021 11:57:33 +0100 Subject: [PATCH 075/110] Fix regression #6375 --- apps/openmw/mwmechanics/creaturestats.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 6710dcd734..d832c47c87 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -539,6 +539,7 @@ namespace MWMechanics state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; + state.mRecalcDynamicStats = false; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; From d88d006984a27de79fbec9c100403d3af339132f Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sun, 31 Oct 2021 11:59:34 +0000 Subject: [PATCH 076/110] fixes getViewDistance (#3207) I have been informed by @akortunov that my addition of `Groundcover::getViewDistance` is not working in some cases. Investigations revealed that some `ViewData` code interacting with my additions had been quite thoroughly optimised in a way that was not sufficiently documented and interfered with the new feature. With this PR we repair `getViewDistance` while preserving such optimisations and add a necessary comment to avoid issues in the future. In addition, we now rebuild views when the global `mViewDistance` changes. --- components/terrain/quadtreeworld.cpp | 36 ++++++++++++++++------- components/terrain/quadtreeworld.hpp | 7 ++++- components/terrain/viewdata.cpp | 35 +++------------------- components/terrain/viewdata.hpp | 43 ++++++++++++++-------------- 4 files changed, 57 insertions(+), 64 deletions(-) diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 0282eb8de1..7fc0895846 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -278,6 +278,7 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour , mViewDistance(std::numeric_limits::max()) , mMinSize(1/8.f) , mDebugTerrainChunks(debugChunks) + , mRevalidateDistance(0.f) { mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapLevel(compMapLevel); @@ -346,22 +347,25 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const return lodFlags; } -void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile, float reuseDistance) +void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance) { if (!vd->hasChanged() && entry.mRenderingNode) return; - int ourLod = getVertexLod(entry.mNode, vertexLodMod); + int ourLod = getVertexLod(entry.mNode, mVertexLodMod); if (vd->hasChanged()) { // have to recompute the lodFlags in case a neighbour has changed LOD. - unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, vertexLodMod, vd); + unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, mVertexLodMod, vd); if (lodFlags != entry.mLodFlags) { entry.mRenderingNode = nullptr; entry.mLodFlags = lodFlags; } + // have to revalidate chunks within a custom view distance. + if (mRevalidateDistance && entry.mNode->distance(vd->getViewPoint()) <= mRevalidateDistance + reuseDistance) + entry.mRenderingNode = nullptr; } if (!entry.mRenderingNode) @@ -372,9 +376,9 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f const osg::Vec2f& center = entry.mNode->getCenter(); bool activeGrid = (center.x() > gridbounds.x() && center.y() > gridbounds.y() && center.x() < gridbounds.z() && center.y() < gridbounds.w()); - for (QuadTreeWorld::ChunkManager* m : chunkManagers) + for (QuadTreeWorld::ChunkManager* m : mChunkManagers) { - if (m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) + if (mRevalidateDistance && m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) continue; osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); if (n) pat->addChild(n); @@ -398,7 +402,7 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil: static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); osg::BoundingBox bb = static_cast(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); if (!bb.valid()) continue; @@ -457,15 +461,15 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, mActiveGrid, mChunkManagers, false, mViewDataMap->getReuseDistance()); + ViewDataEntry& entry = vd->getEntry(i); + loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false, mViewDataMap->getReuseDistance()); entry.mRenderingNode->accept(nv); } if (mHeightCullCallback && isCullVisitor) updateWaterCullingView(mHeightCullCallback, vd, static_cast(&nv), mStorage->getCellWorldSize(), !isGridEmpty()); - vd->markUnchanged(); + vd->setChanged(false); double referenceTime = nv.getFrameStamp() ? nv.getFrameStamp()->getReferenceTime() : 0.0; if (referenceTime != 0.0) @@ -540,9 +544,9 @@ void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg:: const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier)); for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, reuseDistance); + loadRenderingNode(entry, vd, cellWorldSize, grid, true, reuseDistance); if (pass==0) reporter.addProgress(entry.mNode->getSize()); entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass } @@ -579,6 +583,8 @@ void QuadTreeWorld::addChunkManager(QuadTreeWorld::ChunkManager* m) { mChunkManagers.push_back(m); mTerrainRoot->setNodeMask(mTerrainRoot->getNodeMask()|m->getNodeMask()); + if (m->getViewDistance()) + mRevalidateDistance = std::max(m->getViewDistance(), mRevalidateDistance); } void QuadTreeWorld::rebuildViews() @@ -586,4 +592,12 @@ void QuadTreeWorld::rebuildViews() mViewDataMap->rebuildViews(); } +void QuadTreeWorld::setViewDistance(float viewDistance) +{ + if (mViewDistance == viewDistance) + return; + mViewDistance = viewDistance; + mViewDataMap->rebuildViews(); +} + } diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 3bd606d6c6..9d21d65fc5 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -16,6 +16,9 @@ namespace Terrain { class RootNode; class ViewDataMap; + class ViewData; + struct ViewDataEntry; + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. @@ -30,7 +33,7 @@ namespace Terrain void enable(bool enabled) override; - void setViewDistance(float distance) override { mViewDistance = distance; } + void setViewDistance(float distance) override; void cacheCell(View *view, int x, int y) override {} /// @note Not thread safe. @@ -60,6 +63,7 @@ namespace Terrain private: void ensureQuadTreeBuilt(); + void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance); osg::ref_ptr mRootNode; @@ -75,6 +79,7 @@ namespace Terrain float mMinSize; bool mDebugTerrainChunks; std::unique_ptr mDebugChunkManager; + float mRevalidateDistance; }; } diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index e517390b44..ae23f034a8 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -12,12 +12,10 @@ ViewData::ViewData() , mHasViewPoint(false) , mWorldUpdateRevision(0) { - } ViewData::~ViewData() { - } void ViewData::copyFrom(const ViewData& other) @@ -38,42 +36,17 @@ void ViewData::add(QuadTreeNode *node) if (index+1 > mEntries.size()) mEntries.resize(index+1); - Entry& entry = mEntries[index]; + ViewDataEntry& entry = mEntries[index]; if (entry.set(node)) mChanged = true; } -unsigned int ViewData::getNumEntries() const -{ - return mNumEntries; -} - -ViewData::Entry &ViewData::getEntry(unsigned int i) -{ - return mEntries[i]; -} - -bool ViewData::hasChanged() const -{ - return mChanged; -} - -bool ViewData::hasViewPoint() const -{ - return mHasViewPoint; -} - void ViewData::setViewPoint(const osg::Vec3f &viewPoint) { mViewPoint = viewPoint; mHasViewPoint = true; } -const osg::Vec3f& ViewData::getViewPoint() const -{ - return mViewPoint; -} - // NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here. // If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset(). void ViewData::reset() @@ -110,14 +83,13 @@ bool ViewData::contains(QuadTreeNode *node) const return false; } -ViewData::Entry::Entry() +ViewDataEntry::ViewDataEntry() : mNode(nullptr) , mLodFlags(0) { - } -bool ViewData::Entry::set(QuadTreeNode *node) +bool ViewDataEntry::set(QuadTreeNode *node) { if (node == mNode) return false; @@ -173,6 +145,7 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo } vd->setViewPoint(viewPoint); vd->setActiveGrid(activeGrid); + vd->setChanged(true); needsUpdate = true; } } diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 5d814251ea..b7dbc977b1 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -13,6 +13,18 @@ namespace Terrain class QuadTreeNode; + struct ViewDataEntry + { + ViewDataEntry(); + + bool set(QuadTreeNode* node); + + QuadTreeNode* mNode; + + unsigned int mLodFlags; + osg::ref_ptr mRenderingNode; + }; + class ViewData : public View { public: @@ -31,33 +43,22 @@ namespace Terrain void copyFrom(const ViewData& other); - struct Entry - { - Entry(); - - bool set(QuadTreeNode* node); - - QuadTreeNode* mNode; - - unsigned int mLodFlags; - osg::ref_ptr mRenderingNode; - }; - - unsigned int getNumEntries() const; - - Entry& getEntry(unsigned int i); + unsigned int getNumEntries() const { return mNumEntries; } + ViewDataEntry& getEntry(unsigned int i) { return mEntries[i]; } double getLastUsageTimeStamp() const { return mLastUsageTimeStamp; } void setLastUsageTimeStamp(double timeStamp) { mLastUsageTimeStamp = timeStamp; } - /// @return Have any nodes changed since the last frame - bool hasChanged() const; - void markUnchanged() { mChanged = false; } + /// Indicates at least one mNode of mEntries has changed or the view point has moved beyond mReuseDistance. + /// @note Such changes may necessitate a revalidation of cached mRenderingNodes elsewhere depending + /// on the parameters that affect the creation of mRenderingNode. + bool hasChanged() const { return mChanged; } + void setChanged(bool changed) { mChanged = changed; } - bool hasViewPoint() const; + bool hasViewPoint() const { return mHasViewPoint; } void setViewPoint(const osg::Vec3f& viewPoint); - const osg::Vec3f& getViewPoint() const; + const osg::Vec3f& getViewPoint() const { return mViewPoint; } void setActiveGrid(const osg::Vec4i &grid) { if (grid != mActiveGrid) {mActiveGrid = grid;mEntries.clear();mNumEntries=0;} } const osg::Vec4i &getActiveGrid() const { return mActiveGrid;} @@ -66,7 +67,7 @@ namespace Terrain void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; } private: - std::vector mEntries; + std::vector mEntries; unsigned int mNumEntries; double mLastUsageTimeStamp; bool mChanged; From b9911da4c799984c639102e5dcae133bd52ce378 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Sun, 31 Oct 2021 12:03:42 +0000 Subject: [PATCH 077/110] applies lightMask (#3201) With this PR we apply `lightMask` to a `Transform` node we create specifically for a light. This mask will allow us to stop traversing such nodes sooner and avoid costly processing associated with `Transform` nodes in the cull visitor. --- components/sceneutil/lightutil.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index 6a1a1376ec..2a5a945558 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -79,6 +79,7 @@ namespace SceneUtil // PositionAttitudeTransform seems to be slightly faster than MatrixTransform osg::ref_ptr trans(new SceneUtil::PositionAttitudeTransform); trans->setPosition(computeBound.getBoundingBox().center()); + trans->setNodeMask(lightMask); node->addChild(trans); From e7ec89573e437e66b8fc26aaf3fab1903183a20e Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 31 Oct 2021 17:38:06 +0100 Subject: [PATCH 078/110] Refactoring. Lua `Callback` is moved from apps/openmw/mwlua to components/lua. --- apps/openmw/mwlua/asyncbindings.cpp | 5 ++- apps/openmw/mwlua/luamanagerimp.cpp | 10 ------ apps/openmw/mwlua/luamanagerimp.hpp | 22 ++++-------- components/lua/scriptscontainer.cpp | 52 +++++++++++++++++------------ components/lua/scriptscontainer.hpp | 26 +++++++++++---- 5 files changed, 59 insertions(+), 56 deletions(-) diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index d438518452..9bddf75ee4 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -52,13 +52,12 @@ namespace MWLua }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { - return Callback{std::move(fn), asyncId.mHiddenData}; + return LuaUtil::Callback{std::move(fn), asyncId.mHiddenData}; }; auto initializer = [](sol::table hiddenData) { - LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; - hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); + LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 5695883691..4e47bf7167 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -78,16 +78,6 @@ namespace MWLua mInitialized = true; } - void Callback::operator()(sol::object arg) const - { - if (mHiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY] != sol::nil) - LuaUtil::call(mFunc, std::move(arg)); - else - { - Log(Debug::Debug) << "Ignored callback to removed script " << mHiddenData.get(SCRIPT_NAME_KEY); - } - } - void LuaManager::update(bool paused, float dt) { ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index b9151de685..f5ffe9d258 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -19,19 +19,6 @@ namespace MWLua { - // Wrapper for a single-argument Lua function. - // Holds information about the script the function belongs to. - // Needed to prevent callback calls if the script was removed. - struct Callback - { - static constexpr std::string_view SCRIPT_NAME_KEY = "name"; - - sol::function mFunc; - sol::table mHiddenData; - - void operator()(sol::object arg) const; - }; - class LuaManager : public MWBase::LuaManager { public: @@ -82,13 +69,16 @@ namespace MWLua void reloadAllScripts() override; // Used to call Lua callbacks from C++ - void queueCallback(Callback callback, sol::object arg) { mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); } + void queueCallback(LuaUtil::Callback callback, sol::object arg) + { + mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); + } // Wraps Lua callback into an std::function. // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or // any other Lua-related function is running. template - std::function wrapLuaCallback(const Callback& c) + std::function wrapLuaCallback(const LuaUtil::Callback& c) { return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; } @@ -131,7 +121,7 @@ namespace MWLua struct CallbackWithData { - Callback mCallback; + LuaUtil::Callback mCallback; sol::object mArg; }; std::vector mQueuedCallbacks; diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index c92116f1d9..517ad5f788 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -15,15 +15,6 @@ namespace LuaUtil static constexpr std::string_view HANDLER_LOAD = "onLoad"; static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; - std::string ScriptsContainer::ScriptId::toString() const - { - std::string res = mContainer->mNamePrefix; - res.push_back('['); - res.append(mPath); - res.push_back(']'); - return res; - } - ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode) : mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode) { @@ -70,11 +61,19 @@ namespace LuaUtil return false; // already present const std::string& path = scriptPath(scriptId); + std::string debugName = mNamePrefix; + debugName.push_back('['); + debugName.append(path); + debugName.push_back(']'); + + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[sScriptIdKey] = ScriptId{this, scriptId}; + script.mHiddenData[sScriptDebugNameKey] = debugName; + script.mPath = path; + try { - Script& script = mScripts[scriptId]; - script.mHiddenData = mLua.newTable(); - script.mHiddenData[ScriptId::KEY] = ScriptId{this, scriptId, path}; sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); if (scriptOutput == sol::nil) return true; @@ -91,7 +90,7 @@ namespace LuaUtil else if (sectionName == INTERFACE) script.mInterface = value.as(); else - Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << debugName; } if (engineHandlers != sol::nil) { @@ -110,8 +109,7 @@ namespace LuaUtil { auto it = mEngineHandlers.find(handlerName); if (it == mEngineHandlers.end()) - Log(Debug::Error) << "Not supported handler '" << handlerName - << "' in " << mNamePrefix << "[" << path << "]"; + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << debugName; else insertHandler(it->second->mList, scriptId, fn); } @@ -131,7 +129,7 @@ namespace LuaUtil if (script.mInterfaceName.empty() == script.mInterface.has_value()) { - Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; + Log(Debug::Error) << debugName << ": 'interfaceName' should always be used together with 'interface'"; script.mInterfaceName.clear(); script.mInterface = sol::nil; } @@ -145,8 +143,9 @@ namespace LuaUtil } catch (std::exception& e) { + mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil; mScripts.erase(scriptId); - Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); + Log(Debug::Error) << "Can't start " << debugName << "; " << e.what(); return false; } } @@ -159,7 +158,7 @@ namespace LuaUtil Script& script = scriptIter->second; if (script.mInterface) removeInterface(scriptId, script); - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; mScripts.erase(scriptIter); for (auto& [_, handlers] : mEngineHandlers) removeHandler(handlers->mList, scriptId); @@ -329,7 +328,9 @@ namespace LuaUtil for (auto& [scriptId, script] : mScripts) { ESM::LuaScript savedScript; - savedScript.mScriptPath = script.mHiddenData.get(ScriptId::KEY).mPath; + // Note: We can not use `scriptPath(scriptId)` here because `save` can be called during + // evaluating "reloadlua" command when ScriptsConfiguration is already changed. + savedScript.mScriptPath = script.mPath; if (script.mOnSave) { try @@ -423,7 +424,7 @@ namespace LuaUtil ScriptsContainer::~ScriptsContainer() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; } // Note: shouldn't be called from destructor because mEngineHandlers has pointers on @@ -431,7 +432,7 @@ namespace LuaUtil void ScriptsContainer::removeAllScripts() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; mScripts.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); @@ -529,4 +530,13 @@ namespace LuaUtil updateTimerQueue(mHoursTimersQueue, gameHours); } + void Callback::operator()(sol::object arg) const + { + if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil) + LuaUtil::call(mFunc, std::move(arg)); + else + Log(Debug::Debug) << "Ignored callback to the removed script " + << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); + } + } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index b25c69b5b4..e934868d08 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -60,16 +60,18 @@ namespace LuaUtil class ScriptsContainer { public: + // ScriptId of each script is stored with this key in Script::mHiddenData. + // Removed from mHiddenData when the script if removed. + constexpr static std::string_view sScriptIdKey = "_id"; + + // Debug identifier of each script is stored with this key in Script::mHiddenData. + // Present in mHiddenData even after removal of the script from ScriptsContainer. + constexpr static std::string_view sScriptDebugNameKey = "_name"; + struct ScriptId { - // ScriptId is stored in hidden data (see getHiddenData) with this key. - constexpr static std::string_view KEY = "_id"; - ScriptsContainer* mContainer; int mIndex; // index in LuaUtil::ScriptsConfiguration - std::string mPath; // path to the script source in VFS - - std::string toString() const; }; using TimeUnit = ESM::LuaTimer::TimeUnit; @@ -192,6 +194,7 @@ namespace LuaUtil sol::table mHiddenData; std::map mRegisteredCallbacks; std::map mTemporaryCallbacks; + std::string mPath; }; struct Timer { @@ -239,6 +242,17 @@ namespace LuaUtil int64_t mTemporaryCallbackCounter = 0; }; + // Wrapper for a single-argument Lua function. + // Holds information about the script the function belongs to. + // Needed to prevent callback calls if the script was removed. + struct Callback + { + sol::function mFunc; + sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer + + void operator()(sol::object arg) const; + }; + } #endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H From 92bdd44e58941e2e681fde4f590416db6ce239e1 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 31 Oct 2021 20:25:37 +0100 Subject: [PATCH 079/110] Allow creatures to use torches --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a574338a..11e4e0ae26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Bug #6326: Detect Enchantment/Key should detect items in unresolved containers Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures Bug #6363: Some scripts in Morrowland fail to work + Bug #6376: Creatures should be able to use torches Feature #890: OpenMW-CS: Column filtering Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 0af65844f9..cb4f8237e7 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1496,15 +1496,13 @@ namespace MWMechanics stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); } - if(iter->first.getClass().isNpc()) + if(inProcessingRange && iter->first.getClass().isNpc()) { // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe - if (inProcessingRange) - updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - - if (timerUpdateEquippedLight == 0) - updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); + updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); } + if(timerUpdateEquippedLight == 0 && iter->first.getClass().hasInventoryStore(iter->first)) + updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); if (luaControls && isConscious(iter->first)) { From 8c21b0b503386f583e57e2ce40e9cd5e64a0299b Mon Sep 17 00:00:00 2001 From: fredzio Date: Mon, 1 Nov 2021 12:44:36 +0100 Subject: [PATCH 080/110] Apply waterwalking even when we skip simulation. This chunk was supposed to be part of !1324 but somehow got stuck staged in my tree. --- apps/openmw/mwphysics/mtphysics.cpp | 6 ++++-- apps/openmw/mwphysics/physicssystem.cpp | 18 ------------------ apps/openmw/mwphysics/physicssystem.hpp | 1 - 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index a989493f4d..5c49dee297 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -123,8 +123,10 @@ namespace frameData.mPosition = actor->getPosition(); if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) { - frameData.mPosition.z() = frameData.mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(actor->getPtr(), frameData.mPosition, false); + const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z()); + MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset); + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); } frameData.mOldHeight = frameData.mPosition.z(); const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index af6dc109ed..a64ee70b96 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -945,24 +945,6 @@ namespace MWPhysics { } - void ActorFrameData::updatePosition(Actor& actor, btCollisionWorld* world) - { - actor.applyOffsetChange(); - mPosition = actor.getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && actor.canMoveToWaterSurface(mWaterlevel, world)) - { - MWBase::Environment::get().getWorld()->moveObjectBy(actor.getPtr(), osg::Vec3f(0, 0, mWaterlevel - mPosition.z())); - actor.applyOffsetChange(); - mPosition = actor.getPosition(); - } - mOldHeight = mPosition.z(); - const auto rotation = actor.getPtr().getRefData().getPosition().asRotationVec3(); - mRotation = osg::Vec2f(rotation.x(), rotation.z()); - mInertia = actor.getInertialForce(); - mStuckFrames = actor.getStuckFrames(); - mLastStuckPosition = actor.getLastStuckPosition(); - } - ProjectileFrameData::ProjectileFrameData(Projectile& projectile) : mPosition(projectile.getPosition()) , mMovement(projectile.velocity()) diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index a025f1cc8b..c31bbfbf65 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -76,7 +76,6 @@ namespace MWPhysics struct ActorFrameData { ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); - void updatePosition(Actor& actor, btCollisionWorld* world); osg::Vec3f mPosition; osg::Vec3f mInertia; const btCollisionObject* mStandingOn; From 3660d5cc4cf6d2984d203fe459a63d6d690f8bf0 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 1 Nov 2021 19:33:29 +0100 Subject: [PATCH 081/110] Reduce code duplication in getting the shield model --- apps/openmw/mwrender/actoranimation.cpp | 38 +++++++++++++------- apps/openmw/mwrender/actoranimation.hpp | 3 +- apps/openmw/mwrender/creatureanimation.cpp | 15 +------- apps/openmw/mwrender/npcanimation.cpp | 42 ++-------------------- apps/openmw/mwrender/npcanimation.hpp | 2 +- 5 files changed, 32 insertions(+), 68 deletions(-) diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 24ca0aa4f3..e820eb5672 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -84,30 +84,42 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } -std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const { - std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; + // Try to recover the body part model, use ground model as a fallback otherwise. if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); - - // Try to get shield model from bodyparts first, with ground model as fallback for (const auto& part : bodyparts) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + if (part.mPart != ESM::PRT_Shield) continue; - const ESM::BodyPart *bodypart = partStore.search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + + std::string bodypartName; + if (female && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) { - mesh = "meshes\\" + bodypart->mModel; - break; + const ESM::BodyPart *bodypart = partStore.search(bodypartName); + if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return std::string(); + if (!bodypart->mModel.empty()) + return "meshes\\" + bodypart->mModel; } } } + return shield.getClass().getModel(shield); +} + +std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const +{ + std::string mesh = getShieldMesh(shield, false); if (mesh.empty()) return mesh; @@ -143,7 +155,7 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getShieldMesh(*shield).empty()) + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getSheathedShieldMesh(*shield).empty()) { if(stats.getDrawState() != MWMechanics::DrawState_Weapon) return false; @@ -201,7 +213,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) return; } - std::string mesh = getShieldMesh(*shield); + std::string mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; @@ -255,7 +267,7 @@ bool ActorAnimation::useShieldAnimations() const const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (weapon != inv.end() && shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && - !getShieldMesh(*shield).empty()) + !getSheathedShieldMesh(*shield).empty()) { auto type = weapon->getType(); if(type == ESM::Weapon::sRecordId) diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 61ad1ca235..c68ce4dfe2 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -45,7 +45,8 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(const MWWorld::ConstPtr& shield) const; + std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; + virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 657179db75..7dea8173b6 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -139,20 +139,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) bonename = "Shield Bone"; if (item.getType() == ESM::Armor::sRecordId) { - // Shield body part model should be used if possible. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const auto& part : item.get()->mBase->mParts.mParts) - { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = store.get().search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) - { - itemModel = "meshes\\" + bodypart->mModel; - break; - } - } + itemModel = getShieldMesh(item, false); } } diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index f3d026ad3c..16cf4cee69 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -82,34 +82,6 @@ std::string getVampireHead(const std::string& race, bool female) return "meshes\\" + bodyPart->mModel; } -std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); - for (const auto& part : bodyparts) - { - if (part.mPart != ESM::PRT_Shield) - continue; - - std::string bodypartName; - if (female && !part.mFemale.empty()) - bodypartName = part.mFemale; - else if (!part.mMale.empty()) - bodypartName = part.mMale; - - if (!bodypartName.empty()) - { - const ESM::BodyPart *bodypart = partStore.search(bodypartName); - if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return std::string(); - if (!bodypart->mModel.empty()) - return "meshes\\" + bodypart->mModel; - } - } - - return std::string(); -} - } @@ -547,14 +519,9 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } -std::string NpcAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - // Try to recover the body part model, use ground model as a fallback otherwise. - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + std::string mesh = getShieldMesh(shield, !mNpc->isMale()); if (mesh.empty()) return std::string(); @@ -1011,10 +978,7 @@ void NpcAnimation::showCarriedLeft(bool show) // For shields we must try to use the body part model if (iter->getType() == ESM::Armor::sRecordId) { - const ESM::Armor *armor = iter->get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + mesh = getShieldMesh(*iter, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index b511a52f37..d0d9a26fdc 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -105,7 +105,7 @@ private: protected: void addControllers() override; bool isArrowAttached() const override; - std::string getShieldMesh(const MWWorld::ConstPtr& shield) const override; + std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override; public: /** From 665d756f02df18d91f2273be82fb8f5e8851de9e Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 2 Nov 2021 00:05:22 +0100 Subject: [PATCH 082/110] Fix readthedocs config --- .readthedocs.yaml | 10 ++++++++++ docs/requirements.txt | 2 +- readthedocs.yml | 2 -- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 readthedocs.yml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..e0b39ec495 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + diff --git a/docs/requirements.txt b/docs/requirements.txt index 288d462d0d..14c09d53e7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ parse_cmake -sphinx>=1.7.0 +sphinx==1.8.5 diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index e53e54b785..0000000000 --- a/readthedocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -# Don't build any extra formats -formats: [] \ No newline at end of file From a5a895ffd482737316165d175ec0e2d316fc9dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20St=C3=B6ckel?= Date: Mon, 1 Nov 2021 18:22:38 -0400 Subject: [PATCH 083/110] Fix #6381 Do not use osg::PI_f --- apps/openmw/mwworld/worldimp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index cad7de9710..ed5872fb6d 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1313,7 +1313,7 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - objRot[0] = osg::clampBetween(objRot[0], -osg::PIf / 2, osg::PIf / 2); + objRot[0] = osg::clampBetween(objRot[0], -osg::PI_2, osg::PI_2); objRot[1] = Misc::normalizeAngle(objRot[1]); objRot[2] = Misc::normalizeAngle(objRot[2]); } From 20f851b3b50ac26059e3cbea87264fb7c0d8ec08 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 2 Nov 2021 10:21:21 +0100 Subject: [PATCH 084/110] Fix readthedocs config, second attempt. --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 14c09d53e7..ac82149f5d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ parse_cmake sphinx==1.8.5 +docutils==0.17.1 From 213faa669521521a6fa137629abb312e6fa1036a Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Tue, 2 Nov 2021 13:46:41 +0000 Subject: [PATCH 085/110] restores countRecords optimisations (#3210) With this PR we restore @elsid 's optimisations of countRecords we have unintentionally discarded in PR #3197. In addition, we give it a more appropriate name and add comments concerning its peculiar background. --- apps/openmw/mwworld/esmstore.cpp | 12 +++++++++--- apps/openmw/mwworld/esmstore.hpp | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 3cedcd457a..5ec95ecf47 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -29,6 +29,7 @@ namespace void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) { + // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { size_t index = cell.mContextList[i].index; @@ -301,12 +302,14 @@ void ESMStore::setUp(bool validateRecords) if (validateRecords) { validate(); - countRecords(); + countAllCellRefs(); } } -void ESMStore::countRecords() +void ESMStore::countAllCellRefs() { + // TODO: We currently need to read entire files here again. + // We should consider consolidating or deferring this reading. if(!mRefCount.empty()) return; std::vector refs; @@ -324,6 +327,8 @@ void ESMStore::countRecords() if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; + // We manually lower case IDs here for the time being to improve performance. + Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } }; @@ -332,7 +337,8 @@ void ESMStore::countRecords() int ESMStore::getRefCount(const std::string& id) const { - auto it = mRefCount.find(id); + const std::string lowerId = Misc::StringUtils::lowerCase(id); + auto it = mRefCount.find(lowerId); if(it == mRefCount.end()) return 0; return it->second; diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index d1a4942cd3..8582a1daca 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -79,7 +79,7 @@ namespace MWWorld IDMap mIds; IDMap mStaticIds; - IDMap mRefCount; + std::unordered_map mRefCount; std::map mStores; @@ -90,7 +90,7 @@ namespace MWWorld /// Validate entries in store after setup void validate(); - void countRecords(); + void countAllCellRefs(); template void removeMissingObjects(Store& store); From 940e338453fd4f03309148ea63f6feed2e5bb376 Mon Sep 17 00:00:00 2001 From: wareya Date: Tue, 2 Nov 2021 15:17:26 +0000 Subject: [PATCH 086/110] Constifications --- apps/openmw/mwphysics/trace.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index e489ea0eb2..b7930bfa53 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -58,7 +58,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star doing_short_trace = true; } - auto traceCallback = sweepHelper(actor, btstart, btend, world, false); + const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: if(traceCallback.hasHit()) @@ -77,7 +77,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star if(doing_short_trace) { btend = Misc::Convert::toBullet(end); - auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); if(newTraceCallback.hasHit()) { @@ -100,11 +100,11 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - auto newTraceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); - if(newTraceCallback.hasHit()) + const auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; } else From a9106f4d7c239adc6ec2ba2968b594ecf238c5c7 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 2 Nov 2021 18:01:22 +0100 Subject: [PATCH 087/110] Rotate torches by 90 degrees --- CHANGELOG.md | 1 + apps/openmw/mwrender/actoranimation.cpp | 17 +++++++++++++++++ apps/openmw/mwrender/actoranimation.hpp | 1 + apps/openmw/mwrender/creatureanimation.cpp | 8 +------- apps/openmw/mwrender/npcanimation.cpp | 19 ++++++------------- apps/openmw/mwrender/npcanimation.hpp | 4 ++-- components/sceneutil/attach.cpp | 11 ++++++++--- components/sceneutil/attach.hpp | 3 ++- 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a574338a..2ae4126b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Bug #5842: GetDisposition adds temporary disposition change from different actors Bug #5863: GetEffect should return true after the player has teleported Bug #5913: Failed assertion during Ritual of Trees quest + Bug #5937: Lights always need to be rotated by 90 degrees Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index e820eb5672..7706f7d7f1 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -84,6 +85,22 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } +osg::ref_ptr ActorAnimation::attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight) +{ + osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); + + const NodeMap& nodeMap = getNodeMap(); + auto found = nodeMap.find(bonename); + if (found == nodeMap.end()) + throw std::runtime_error("Can't find attachment node " + bonename); + if(isLight) + { + osg::Quat rotation(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0)); + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); + } + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); +} + std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const { const ESM::Armor *armor = shield.get()->mBase; diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index c68ce4dfe2..1ece0c326d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -54,6 +54,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); return attachMesh(model, bonename, false, &stubColor); }; + osg::ref_ptr attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 7dea8173b6..50dfb68008 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -145,13 +145,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(itemModel); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(bonename); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get(), mResourceSystem->getSceneManager()); + osg::ref_ptr attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); scene.reset(new PartHolder(attached)); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 16cf4cee69..0d6c21c308 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -645,7 +645,7 @@ void NpcAnimation::updateParts() { const ESM::Light *light = part.get()->mBase; addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, - 1, "meshes\\"+light->mModel); + 1, "meshes\\"+light->mModel, false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); } @@ -675,16 +675,9 @@ void NpcAnimation::updateParts() -PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) +PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(bonename); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - - osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); + osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); @@ -757,7 +750,7 @@ bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } -bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) +bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if(priority <= mPartPriorities[type]) return false; @@ -789,7 +782,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; - mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); + mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight); } catch (std::exception& e) { @@ -981,7 +974,7 @@ void NpcAnimation::showCarriedLeft(bool show) mesh = getShieldMesh(*iter, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) + mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId)) { if (mesh.empty()) reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index d0d9a26fdc..2dcfac3036 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -83,13 +83,13 @@ private: NpcType getNpcType() const; 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, bool isLight); void removeIndividualPart(ESM::PartReferenceType type); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, - bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); + bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr, bool isLight = false); void removePartGroup(int group); void addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 9dabb282b0..02c3456425 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -100,7 +100,7 @@ namespace SceneUtil } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager, const osg::Quat* attitude) { if (dynamic_cast(toAttach.get())) { @@ -144,8 +144,6 @@ namespace SceneUtil trans = new osg::PositionAttitudeTransform; trans->setPosition(boneOffset->getMatrix().getTrans()); - // The BoneOffset rotation seems to be incorrect - trans->setAttitude(osg::Quat(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0))); // Now that we used it, get rid of the redundant node. if (boneOffset->getNumChildren() == 0 && boneOffset->getNumParents() == 1) @@ -172,6 +170,13 @@ namespace SceneUtil trans->setStateSet(frontFaceStateSet); } + if(attitude) + { + if (!trans) + trans = new osg::PositionAttitudeTransform; + trans->setAttitude(*attitude); + } + if (trans) { attachNode->addChild(trans); diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index f5fc693724..ed0299dece 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -9,6 +9,7 @@ namespace osg { class Node; class Group; + class Quat; } namespace Resource { @@ -23,7 +24,7 @@ namespace SceneUtil /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager, const osg::Quat* attitude = nullptr); } From 4631d957393059fef18c2cf98e2d6d93140c971c Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 15:19:19 +0100 Subject: [PATCH 088/110] Add more tests for BulletNifLoader --- .../nifloader/testbulletnifloader.cpp | 214 +++++++++++++++++- components/nifbullet/bulletnifloader.cpp | 9 +- 2 files changed, 216 insertions(+), 7 deletions(-) diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index 3d4628c267..3d9b56c930 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -60,6 +60,19 @@ namespace { return isNear(lhs.getOrigin(), rhs.getOrigin()) && isNear(lhs.getBasis(), rhs.getBasis()); } + + struct WriteVec3f + { + osg::Vec3f mValue; + + friend std::ostream& operator <<(std::ostream& stream, const WriteVec3f& value) + { + return stream << "osg::Vec3f {" + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.x() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.y() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.z() << "}"; + } + }; } static std::ostream& operator <<(std::ostream& stream, const btVector3& value) @@ -122,6 +135,17 @@ static std::ostream& operator <<(std::ostream& stream, const TriangleMeshShape& return stream << "}}"; } +static bool operator ==(const BulletShape::CollisionBox& l, const BulletShape::CollisionBox& r) +{ + const auto tie = [] (const BulletShape::CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; + return tie(l) == tie(r); +} + +static std::ostream& operator <<(std::ostream& stream, const BulletShape::CollisionBox& value) +{ + return stream << "CollisionBox {" << WriteVec3f {value.mExtents} << ", " << WriteVec3f {value.mCenter} << "}"; +} + } static std::ostream& operator <<(std::ostream& stream, const btCollisionShape& value) @@ -162,8 +186,7 @@ namespace Resource { return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) - && lhs.mCollisionBox.mExtents == rhs.mCollisionBox.mExtents - && lhs.mCollisionBox.mCenter == rhs.mCollisionBox.mCenter + && lhs.mCollisionBox == rhs.mCollisionBox && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } @@ -172,8 +195,7 @@ namespace Resource return stream << "Resource::BulletShape {" << value.mCollisionShape.get() << ", " << value.mAvoidCollisionShape.get() << ", " - << "osg::Vec3f {" << value.mCollisionBox.mExtents << "}" << ", " - << "osg::Vec3f {" << value.mCollisionBox.mCenter << "}" << ", " + << value.mCollisionBox << ", " << value.mAnimatedShapes << "}"; } @@ -272,6 +294,12 @@ namespace value.recType = Nif::RC_NiTriShape; } + void init(Nif::NiTriStrips& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriStrips; + } + void init(Nif::NiSkinInstance& value) { value.data = Nif::NiSkinDataPtr(nullptr); @@ -330,6 +358,8 @@ namespace Nif::NiTriShape mNiTriShape; Nif::NiTriShapeData mNiTriShapeData2; Nif::NiTriShape mNiTriShape2; + Nif::NiTriStripsData mNiTriStripsData; + Nif::NiTriStrips mNiTriStrips; Nif::NiSkinInstance mNiSkinInstance; Nif::NiStringExtraData mNiStringExtraData; Nif::NiStringExtraData mNiStringExtraData2; @@ -361,6 +391,7 @@ namespace init(mNiNode3); init(mNiTriShape); init(mNiTriShape2); + init(mNiTriStrips); init(mNiSkinInstance); init(mNiStringExtraData); init(mNiStringExtraData2); @@ -375,6 +406,11 @@ namespace mNiTriShapeData2.vertices = {osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1)}; mNiTriShapeData2.triangles = {0, 1, 2}; mNiTriShape2.data = Nif::NiGeometryDataPtr(&mNiTriShapeData2); + + mNiTriStripsData.recType = Nif::RC_NiTriStripsData; + mNiTriStripsData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0)}; + mNiTriStripsData.strips = {{0, 1, 2, 3}}; + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); } }; @@ -389,6 +425,18 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(nullptr)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + TEST_F(TestBulletNifLoader, for_default_root_nif_node_should_return_default) { EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -860,6 +908,34 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F(TestBulletNifLoader, should_add_static_mesh_to_existing_compound_mesh) + { + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriShape2)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } + TEST_F(TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) { mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); @@ -983,4 +1059,134 @@ namespace EXPECT_EQ(*result, expected); } + + TEST_F(TestBulletNifLoader, should_ignore_tri_shape_data_with_mismatching_data_rec_type) + { + mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriShape)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_shape_with_triangle_mesh_shape) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_mismatching_data_rec_type) + { + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) + { + mNiTriStripsData.strips.clear(); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiNode.recType = Nif::RC_AvoidNode; + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_animated_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriStrips.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriStrips)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(new btCompoundShape); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_not_add_static_mesh_with_no_triangles_to_compound_shape) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } } diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index f808877a75..544ec6b0b4 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -170,7 +170,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) if (mCompoundShape) { - if (mStaticMesh) + if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { btTransform trans; trans.setIdentity(); @@ -181,13 +181,13 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) } mShape->mCollisionShape = std::move(mCompoundShape); } - else if (mStaticMesh) + else if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { mShape->mCollisionShape.reset(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); mStaticMesh.release(); } - if (mAvoidStaticMesh) + if (mAvoidStaticMesh != nullptr && mAvoidStaticMesh->getNumTriangles() > 0) { mShape->mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false)); mAvoidStaticMesh.release(); @@ -376,6 +376,9 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons fillTriangleMesh(*childMesh, niGeometry); + if (childMesh->getNumTriangles() == 0) + return; + std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); From 56eef691a84846fd0076a01acc6be5ac40cda171 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 14:23:44 +0100 Subject: [PATCH 089/110] Use reference type to pass nif node as argument where nullptr is not handled --- components/nifbullet/bulletnifloader.cpp | 109 +++++++++++------------ components/nifbullet/bulletnifloader.hpp | 8 +- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 544ec6b0b4..ed5eba819f 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -18,11 +18,11 @@ namespace { -osg::Matrixf getWorldTransform(const Nif::Node *node) +osg::Matrixf getWorldTransform(const Nif::Node& node) { - if(node->parent != nullptr) - return node->trafo.toMatrix() * getWorldTransform(node->parent); - return node->trafo.toMatrix(); + if(node.parent != nullptr) + return node.trafo.toMatrix() * getWorldTransform(*node.parent); + return node.trafo.toMatrix(); } bool pathFileNameStartsWithX(const std::string& path) @@ -99,12 +99,12 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry* geometry, const osg::Matrixf &transform = osg::Matrixf()) +void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform = osg::Matrixf()) { - if (geometry->recType == Nif::RC_NiTriShape || geometry->recType == Nif::RC_BSLODTriShape) - fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); - else if (geometry->recType == Nif::RC_NiTriStrips) - fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); + if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) + fillTriangleMesh(mesh, static_cast(geometry.data.get()), transform); + else if (geometry.recType == Nif::RC_NiTriStrips) + fillTriangleMesh(mesh, static_cast(geometry.data.get()), transform); } } @@ -141,7 +141,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Try to find a valid bounding box first. If one's found for any root node, use that. for (const Nif::Node* node : roots) { - if (findBoundingBox(node, filename)) + if (findBoundingBox(*node, filename)) { const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); @@ -164,8 +164,8 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // from the collision data present in every root node. for (const Nif::Node* node : roots) { - bool autogenerated = hasAutoGeneratedCollision(node); - handleNode(filename, node, 0, autogenerated, isAnimated, autogenerated); + bool autogenerated = hasAutoGeneratedCollision(*node); + handleNode(filename, *node, 0, autogenerated, isAnimated, autogenerated); } if (mCompoundShape) @@ -198,41 +198,40 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Find a boundingBox in the node hierarchy. // Return: use bounding box for collision? -bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& filename) +bool BulletNifLoader::findBoundingBox(const Nif::Node& node, const std::string& filename) { - if (node->hasBounds) + if (node.hasBounds) { - unsigned int type = node->bounds.type; + unsigned int type = node.bounds.type; switch (type) { case Nif::NiBoundingVolume::Type::BOX_BV: - mShape->mCollisionBox.mExtents = node->bounds.box.extents; - mShape->mCollisionBox.mCenter = node->bounds.box.center; + mShape->mCollisionBox.mExtents = node.bounds.box.extents; + mShape->mCollisionBox.mCenter = node.bounds.box.center; break; default: { std::stringstream warning; - warning << "Unsupported NiBoundingVolume type " << type << " in node " << node->recIndex; + warning << "Unsupported NiBoundingVolume type " << type << " in node " << node.recIndex; warning << " in file " << filename; warn(warning.str()); } } - if (node->flags & Nif::NiNode::Flag_BBoxCollision) + if (node.flags & Nif::NiNode::Flag_BBoxCollision) { return true; } } - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) { - if (findBoundingBox(list[i].getPtr(), filename)) + if (findBoundingBox(list[i].get(), filename)) return true; } } @@ -240,10 +239,9 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& return false; } -bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) +bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node& rootNode) { - const Nif::NiNode *ninode = dynamic_cast(rootNode); - if(ninode) + if (const Nif::NiNode* ninode = dynamic_cast(&rootNode)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) @@ -258,32 +256,32 @@ bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) return true; } -void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *node, int flags, +void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid) { // TODO: allow on-the fly collision switching via toggling this flag - if (node->recType == Nif::RC_NiCollisionSwitch && !(node->flags & Nif::NiNode::Flag_ActiveCollision)) + if (node.recType == Nif::RC_NiCollisionSwitch && !(node.flags & Nif::NiNode::Flag_ActiveCollision)) return; // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. - flags |= node->flags; + flags |= node.flags; - if (!node->controller.empty() && node->controller->recType == Nif::RC_NiKeyframeController - && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) + if (!node.controller.empty() && node.controller->recType == Nif::RC_NiKeyframeController + && (node.controller->flags & Nif::NiNode::ControllerFlag_Active)) isAnimated = true; - isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); + isCollisionNode = isCollisionNode || (node.recType == Nif::RC_RootCollisionNode); // Don't collide with AvoidNode shapes - avoid = avoid || (node->recType == Nif::RC_AvoidNode); + avoid = avoid || (node.recType == Nif::RC_AvoidNode); // We encountered a RootCollisionNode inside autogenerated mesh. It is not right. - if (node->recType == Nif::RC_RootCollisionNode && autogenerated) + if (node.recType == Nif::RC_RootCollisionNode && autogenerated) Log(Debug::Info) << "RootCollisionNode is not attached to the root node in " << fileName << ". Treating it as a common NiTriShape."; // Check for extra data - for (Nif::ExtraPtr e = node->extra; !e.empty(); e = e->next) + for (Nif::ExtraPtr e = node.extra; !e.empty(); e = e->next) { if (e->recType == Nif::RC_NiStringExtraData) { @@ -310,61 +308,58 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *n // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. // (occurs in tr_ex_imp_wall_arch_04.nif) - if(!node->hasBounds && (node->recType == Nif::RC_NiTriShape - || node->recType == Nif::RC_NiTriStrips - || node->recType == Nif::RC_BSLODTriShape)) + if(!node.hasBounds && (node.recType == Nif::RC_NiTriShape + || node.recType == Nif::RC_NiTriStrips + || node.recType == Nif::RC_BSLODTriShape)) { handleNiTriShape(node, flags, getWorldTransform(node), isAnimated, avoid); } } // For NiNodes, loop through children - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) - handleNode(fileName, list[i].getPtr(), flags, isCollisionNode, isAnimated, autogenerated, avoid); + handleNode(fileName, list[i].get(), flags, isCollisionNode, isAnimated, autogenerated, avoid); } } } -void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf &transform, +void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf &transform, bool isAnimated, bool avoid) { - assert(nifNode != nullptr); - // If the object was marked "NCO" earlier, it shouldn't collide with // anything. So don't do anything. if ((flags & 0x800)) return; - auto niGeometry = static_cast(nifNode); - if (niGeometry->data.empty() || niGeometry->data->vertices.empty()) + const Nif::NiGeometry& niGeometry = static_cast(nifNode); + if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) return; - if (niGeometry->recType == Nif::RC_NiTriShape || niGeometry->recType == Nif::RC_BSLODTriShape) + if (niGeometry.recType == Nif::RC_NiTriShape || niGeometry.recType == Nif::RC_BSLODTriShape) { - if (niGeometry->data->recType != Nif::RC_NiTriShapeData) + if (niGeometry.data->recType != Nif::RC_NiTriShapeData) return; - auto data = static_cast(niGeometry->data.getPtr()); + auto data = static_cast(niGeometry.data.getPtr()); if (data->triangles.empty()) return; } - else if (niGeometry->recType == Nif::RC_NiTriStrips) + else if (niGeometry.recType == Nif::RC_NiTriStrips) { - if (niGeometry->data->recType != Nif::RC_NiTriStripsData) + if (niGeometry.data->recType != Nif::RC_NiTriStripsData) return; - auto data = static_cast(niGeometry->data.getPtr()); + auto data = static_cast(niGeometry.data.getPtr()); if (data->strips.empty()) return; } - if (!niGeometry->skin.empty()) + if (!niGeometry.skin.empty()) isAnimated = false; if (isAnimated) @@ -382,20 +377,16 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); - float scale = nifNode->trafo.scale; - const Nif::Node* parent = nifNode; - while (parent->parent) - { - parent = parent->parent; + float scale = nifNode.trafo.scale; + for (const Nif::Node* parent = nifNode.parent; parent != nullptr; parent = parent->parent) scale *= parent->trafo.scale; - } osg::Quat q = transform.getRotate(); osg::Vec3f v = transform.getTrans(); childShape->setLocalScaling(btVector3(scale, scale, scale)); btTransform trans(btQuaternion(q.x(), q.y(), q.z(), q.w()), btVector3(v.x(), v.y(), v.z())); - mShape->mAnimatedShapes.emplace(nifNode->recIndex, mCompoundShape->getNumChildShapes()); + mShape->mAnimatedShapes.emplace(nifNode.recIndex, mCompoundShape->getNumChildShapes()); mCompoundShape->addChildShape(trans, childShape.get()); childShape.release(); diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 3d6a95e09f..5a17a4240c 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -52,14 +52,14 @@ public: osg::ref_ptr load(const Nif::File& file); private: - bool findBoundingBox(const Nif::Node* node, const std::string& filename); + bool findBoundingBox(const Nif::Node& node, const std::string& filename); - void handleNode(const std::string& fileName, Nif::Node const *node, int flags, bool isCollisionNode, + void handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated=false, bool autogenerated=false, bool avoid=false); - bool hasAutoGeneratedCollision(const Nif::Node *rootNode); + bool hasAutoGeneratedCollision(const Nif::Node& rootNode); - void handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); + void handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); std::unique_ptr mCompoundShape; From 4ac83f4c39beab8728a81b83903398efaa3f88d7 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 14:27:16 +0100 Subject: [PATCH 090/110] Add separate function to handle NiGeometry node To force use a single source of data. All fields of Nif::Node are available in NiGeometry. --- components/nifbullet/bulletnifloader.cpp | 13 +++++++++---- components/nifbullet/bulletnifloader.hpp | 3 +++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index ed5eba819f..4bafa8af74 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -336,7 +336,12 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, cons if ((flags & 0x800)) return; - const Nif::NiGeometry& niGeometry = static_cast(nifNode); + handleNiTriShape(static_cast(nifNode), transform, isAnimated, avoid); +} + +void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const osg::Matrixf &transform, + bool isAnimated, bool avoid) +{ if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) return; @@ -377,8 +382,8 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, cons std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); - float scale = nifNode.trafo.scale; - for (const Nif::Node* parent = nifNode.parent; parent != nullptr; parent = parent->parent) + float scale = niGeometry.trafo.scale; + for (const Nif::Node* parent = niGeometry.parent; parent != nullptr; parent = parent->parent) scale *= parent->trafo.scale; osg::Quat q = transform.getRotate(); osg::Vec3f v = transform.getTrans(); @@ -386,7 +391,7 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, cons btTransform trans(btQuaternion(q.x(), q.y(), q.z(), q.w()), btVector3(v.x(), v.y(), v.z())); - mShape->mAnimatedShapes.emplace(nifNode.recIndex, mCompoundShape->getNumChildShapes()); + mShape->mAnimatedShapes.emplace(niGeometry.recIndex, mCompoundShape->getNumChildShapes()); mCompoundShape->addChildShape(trans, childShape.get()); childShape.release(); diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 5a17a4240c..e0fec338c6 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -27,6 +27,7 @@ namespace Nif struct Transformation; struct NiTriShape; struct NiTriStrips; + struct NiGeometry; } namespace NifBullet @@ -61,6 +62,8 @@ private: void handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); + void handleNiTriShape(const Nif::NiGeometry& nifNode, const osg::Matrixf& transform, bool isAnimated, bool avoid); + std::unique_ptr mCompoundShape; std::unique_ptr mStaticMesh; From 4e8e8304aae8834aa8868548e18aba42bf79a2fc Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 14:59:35 +0100 Subject: [PATCH 091/110] Avoid mesh allocation when data is invalid --- .../nifloader/testbulletnifloader.cpp | 3 +- components/nifbullet/bulletnifloader.cpp | 61 +++++++++++-------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index 3d9b56c930..eec09d9a5d 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -1159,8 +1159,7 @@ namespace EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); const auto result = mLoader.load(mNifFile); - Resource::BulletShape expected; - expected.mCollisionShape.reset(new btCompoundShape); + const Resource::BulletShape expected; EXPECT_EQ(*result, expected); } diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 4bafa8af74..655199afb9 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -1,6 +1,7 @@ #include "bulletnifloader.hpp" #include +#include #include #include @@ -99,12 +100,38 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform = osg::Matrixf()) +template +auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) + -> decltype(function(static_cast(geometry.data.get()))) { if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) - fillTriangleMesh(mesh, static_cast(geometry.data.get()), transform); - else if (geometry.recType == Nif::RC_NiTriStrips) - fillTriangleMesh(mesh, static_cast(geometry.data.get()), transform); + return function(static_cast(geometry.data.get())); + + if (geometry.recType == Nif::RC_NiTriStrips) + return function(static_cast(geometry.data.get())); + + return {}; +} + +std::monostate fillTriangleMesh(std::unique_ptr& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + if (mesh == nullptr) + mesh.reset(new btTriangleMesh(false)); + fillTriangleMesh(*mesh, data, transform); + return std::monostate {}; + }); +} + +std::unique_ptr makeChildMesh(const Nif::NiGeometry& geometry) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + std::unique_ptr mesh(new btTriangleMesh); + fillTriangleMesh(*mesh, data, osg::Matrixf()); + return mesh; + }); } } @@ -369,16 +396,13 @@ void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const if (isAnimated) { + std::unique_ptr childMesh = makeChildMesh(niGeometry); + if (childMesh == nullptr || childMesh->getNumTriangles() == 0) + return; + if (!mCompoundShape) mCompoundShape.reset(new btCompoundShape); - std::unique_ptr childMesh(new btTriangleMesh); - - fillTriangleMesh(*childMesh, niGeometry); - - if (childMesh->getNumTriangles() == 0) - return; - std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); @@ -397,20 +421,9 @@ void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const childShape.release(); } else if (avoid) - { - if (!mAvoidStaticMesh) - mAvoidStaticMesh.reset(new btTriangleMesh(false)); - - fillTriangleMesh(*mAvoidStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mAvoidStaticMesh, niGeometry, transform); else - { - if (!mStaticMesh) - mStaticMesh.reset(new btTriangleMesh(false)); - - // Static shape, just transform all vertices into position - fillTriangleMesh(*mStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mStaticMesh, niGeometry, transform); } } // namespace NifBullet From 19843af704b53016ed303ec2dc8952bd36f817a0 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 14:55:04 +0100 Subject: [PATCH 092/110] Combine data check with data handling logic --- components/nifbullet/bulletnifloader.cpp | 41 ++++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 655199afb9..5edf69e17d 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -105,10 +105,28 @@ auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) -> decltype(function(static_cast(geometry.data.get()))) { if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) - return function(static_cast(geometry.data.get())); + { + if (geometry.data->recType != Nif::RC_NiTriShapeData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->triangles.empty()) + return {}; + + return function(static_cast(*data)); + } if (geometry.recType == Nif::RC_NiTriStrips) - return function(static_cast(geometry.data.get())); + { + if (geometry.data->recType != Nif::RC_NiTriStripsData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->strips.empty()) + return {}; + + return function(static_cast(*data)); + } return {}; } @@ -372,25 +390,6 @@ void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) return; - if (niGeometry.recType == Nif::RC_NiTriShape || niGeometry.recType == Nif::RC_BSLODTriShape) - { - if (niGeometry.data->recType != Nif::RC_NiTriShapeData) - return; - - auto data = static_cast(niGeometry.data.getPtr()); - if (data->triangles.empty()) - return; - } - else if (niGeometry.recType == Nif::RC_NiTriStrips) - { - if (niGeometry.data->recType != Nif::RC_NiTriStripsData) - return; - - auto data = static_cast(niGeometry.data.getPtr()); - if (data->strips.empty()) - return; - } - if (!niGeometry.skin.empty()) isAnimated = false; From 2b057f5c15f0224731ae052cc47a7720ab063a87 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 31 Oct 2021 20:34:45 +0100 Subject: [PATCH 093/110] Expect nif node children to have parent --- .../nifloader/testbulletnifloader.cpp | 26 ++++++++++++++++++- components/nifbullet/bulletnifloader.cpp | 8 ++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index eec09d9a5d..b37a8cd6c0 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -506,6 +506,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNode)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -531,6 +532,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -561,11 +563,13 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -595,12 +599,14 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.flags |= Nif::NiNode::Flag_BBoxCollision; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -679,6 +685,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_should_return_shape_with_triangle_mesh_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -697,7 +704,9 @@ namespace TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_shape_with_triangle_mesh_shape) { mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); + mNiNode2.parent = &mNiNode; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiTriShape.parent = &mNiNode2; EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); @@ -714,6 +723,8 @@ namespace TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_shape_with_triangle_mesh_shape_with_all_meshes) { + mNiTriShape.parent = &mNiNode; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), Nif::NodePtr(&mNiTriShape2) @@ -736,6 +747,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_shape_with_triangle_mesh_shape) { mNiTriShape.skin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -804,9 +816,11 @@ namespace { copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), @@ -873,6 +887,7 @@ namespace mController.flags |= Nif::NiNode::ControllerFlag_Active; copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; mNiTriShape2.parent = &mNiNode; @@ -889,7 +904,7 @@ namespace const auto result = mLoader.load(mNifFile); std::unique_ptr triangles(new btTriangleMesh(false)); - triangles->addTriangle(btVector3(1, 2, 3), btVector3(4, 2, 3), btVector3(4, 4.632747650146484375, 1.56172335147857666015625)); + triangles->addTriangle(btVector3(4, 8, 12), btVector3(16, 8, 12), btVector3(16, 18.5309906005859375, 6.246893405914306640625)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); mesh->setLocalScaling(btVector3(1, 1, 1)); @@ -938,6 +953,7 @@ namespace TEST_F(TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.recType = Nif::RC_AvoidNode; @@ -957,6 +973,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -973,6 +990,7 @@ namespace { auto data = static_cast(mNiTriShape.data.getPtr()); data->triangles.clear(); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -990,6 +1008,7 @@ namespace mNiStringExtraData.string = "NC___"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -1008,6 +1027,7 @@ namespace mNiStringExtraData2.string = "NC___"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -1025,6 +1045,7 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -1042,8 +1063,10 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode2; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode2.recType = Nif::RC_RootCollisionNode; + mNiNode2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); mNiNode.recType = Nif::RC_NiNode; @@ -1134,6 +1157,7 @@ namespace TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.recType = Nif::RC_AvoidNode; mNiTriStripsData.strips.front() = {0, 1}; diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 5edf69e17d..6678d8ff74 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -1,5 +1,6 @@ #include "bulletnifloader.hpp" +#include #include #include @@ -367,8 +368,11 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& n const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { - if(!list[i].empty()) - handleNode(fileName, list[i].get(), flags, isCollisionNode, isAnimated, autogenerated, avoid); + if (list[i].empty()) + continue; + + assert(list[i].get().parent == &node); + handleNode(fileName, list[i].get(), flags, isCollisionNode, isAnimated, autogenerated, avoid); } } } From 4657c655b10621f2be75a6fc60ea6dcb721d3727 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Wed, 3 Nov 2021 08:02:06 +0000 Subject: [PATCH 094/110] refactors parentFileIndices (#3211) This PR aims to start addressing `ESM` design issues that have silenced errors we incorporated into groundcover `ESM` loading approaches. - We move the resolution of `parentFileIndices` from `ESMStore` to `ESMReader` as suggested in a `TODO` comment. - We improve a highly misleading comment which downplayed the significance of `parentFileIndices`. - We document important preconditions. - We move a user facing error message to the highest level and improve its context. - We remove an inappropriate `setGlobalReaderList` method. We now pass this reader list into the method that requires it. - We remove a thoroughly pointless optimisation of `Store`'s construction that has unnecessarily depended on `getGlobalReaderList`. There should be no functional changes for `master`, but this PR should remove an issue blocking PR #3208. --- apps/openmw/mwworld/esmloader.cpp | 2 +- apps/openmw/mwworld/esmstore.cpp | 40 ++----------------- apps/openmw/mwworld/store.cpp | 5 --- apps/openmw/mwworld/worldimp.cpp | 21 ++++++++++ apps/openmw/mwworld/worldimp.hpp | 1 + apps/openmw_test_suite/mwworld/test_store.cpp | 13 +----- components/esm/esmreader.cpp | 27 ++++++++++++- components/esm/esmreader.hpp | 13 +++--- components/esmloader/load.cpp | 1 - 9 files changed, 60 insertions(+), 63 deletions(-) diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index 06debf4a97..647f5d3609 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -42,7 +42,7 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) ESM::ESMReader lEsm; lEsm.setEncoder(mEncoder); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&mEsm); + lEsm.resolveParentFileIndices(mEsm); lEsm.open(filepath.string()); mEsm[index] = lEsm; mStore.load(mEsm[index], &mListener); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 5ec95ecf47..183b13ca09 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -4,8 +4,6 @@ #include #include -#include - #include #include #include @@ -153,41 +151,9 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) ESM::Dialogue *dialogue = nullptr; // Land texture loading needs to use a separate internal store for each plugin. - // We set the number of plugins here to avoid continual resizes during loading, - // and so we can properly verify if valid plugin indices are being passed to the - // LandTexture Store retrieval methods. - mLandTextures.resize(esm.getGlobalReaderList()->size()); - - /// \todo Move this to somewhere else. ESMReader? - // Cache parent esX files by tracking their indices in the global list of - // all files/readers used by the engine. This will greaty accelerate - // refnumber mangling, as required for handling moved references. - const std::vector &masters = esm.getGameFiles(); - std::vector *allPlugins = esm.getGlobalReaderList(); - for (size_t j = 0; j < masters.size(); j++) { - const ESM::Header::MasterData &mast = masters[j]; - std::string fname = mast.name; - int index = ~0; - for (int i = 0; i < esm.getIndex(); i++) { - ESM::ESMReader& reader = allPlugins->at(i); - if (reader.getFileSize() == 0) - continue; // Content file in non-ESM format - const std::string candidate = reader.getContext().filename; - std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); - if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { - index = i; - break; - } - } - if (index == (int)~0) { - // Tried to load a parent file that has not been loaded yet. This is bad, - // the launcher should have taken care of this. - std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name - + ", but it has not been loaded yet. Please check your load order."; - esm.fail(fstring); - } - esm.addParentFileIndex(index); - } + // We set the number of plugins here so we can properly verify if valid plugin + // indices are being passed to the LandTexture Store retrieval methods. + mLandTextures.resize(mLandTextures.getSize()+1); // Loop through all records while(esm.hasMoreRecs()) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 32458a2e84..06ea97a859 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -293,11 +293,6 @@ namespace MWWorld //========================================================================= Store::Store() { - mStatic.emplace_back(); - LandTextureList <exl = mStatic[0]; - // More than enough to hold Morrowind.esm. Extra lists for plugins will we - // added on-the-fly in a different method. - ltexl.reserve(128); } const ESM::LandTexture *Store::search(size_t index, size_t plugin) const { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 34907691e9..d35f72e80c 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -178,6 +178,10 @@ namespace MWWorld if (mEsm[0].getFormat() == 0) ensureNeededRecords(); + // TODO: We can and should validate before we call loadContentFiles(). + // Currently we validate here to prevent merge conflicts with groundcover ESMStore fixes. + validateMasterFiles(mEsm); + mCurrentDate.reset(new DateTimeManager()); fillGlobalVariables(); @@ -407,6 +411,23 @@ namespace MWWorld } } + void World::validateMasterFiles(const std::vector& readers) + { + for (const auto& esm : readers) + { + assert(esm.getGameFiles().size() == esm.getParentFileIndices().size()); + for (unsigned int i=0; i gmst; diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index a795b4eafd..afad359cfd 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -157,6 +157,7 @@ namespace MWWorld void updateNavigatorObject(const MWPhysics::Object& object); void ensureNeededRecords(); + void validateMasterFiles(const std::vector& readers); void fillGlobalVariables(); diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index 3fe479587c..29240a1f7f 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -29,19 +29,14 @@ struct ContentFileTest : public ::testing::Test readContentFiles(); // load the content files - std::vector readerList; - readerList.resize(mContentFiles.size()); - int index=0; for (const auto & mContentFile : mContentFiles) { ESM::ESMReader lEsm; lEsm.setEncoder(nullptr); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&readerList); lEsm.open(mContentFile.string()); - readerList[index] = lEsm; - mEsmStore.load(readerList[index], &dummyListener); + mEsmStore.load(lEsm, &dummyListener); ++index; } @@ -254,9 +249,6 @@ TEST_F(StoreTest, delete_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); @@ -297,9 +289,6 @@ TEST_F(StoreTest, overwrite_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index dbf713315b..316748b53a 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -1,5 +1,8 @@ #include "esmreader.hpp" +#include +#include + #include namespace ESM @@ -17,7 +20,6 @@ ESM_Context ESMReader::getContext() ESMReader::ESMReader() : mRecordFlags(0) , mBuffer(50*1024) - , mGlobalReaderList(nullptr) , mEncoder(nullptr) , mFileSize(0) { @@ -55,6 +57,29 @@ void ESMReader::clearCtx() mCtx.subName.clear(); } +void ESMReader::resolveParentFileIndices(const std::vector& allPlugins) +{ + mCtx.parentFileIndices.clear(); + const std::vector &masters = getGameFiles(); + for (size_t j = 0; j < masters.size(); j++) { + const Header::MasterData &mast = masters[j]; + std::string fname = mast.name; + int index = getIndex(); + for (int i = 0; i < getIndex(); i++) { + const ESMReader& reader = allPlugins.at(i); + if (reader.getFileSize() == 0) + continue; // Content file in non-ESM format + const std::string candidate = reader.getName(); + std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); + if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { + index = i; + break; + } + } + mCtx.parentFileIndices.push_back(index); + } +} + void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name) { close(); diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index a438dca0cd..92f2a6673b 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -80,13 +80,15 @@ public: // to the individual load() methods. This hack allows to pass this reference // indirectly to the load() method. void setIndex(const int index) { mCtx.index = index;} - int getIndex() {return mCtx.index;} + int getIndex() const {return mCtx.index;} - void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} - std::vector *getGlobalReaderList() {return mGlobalReaderList;} - - void addParentFileIndex(int index) { mCtx.parentFileIndices.push_back(index); } + // Assign parent esX files by tracking their indices in the global list of + // all files/readers used by the engine. This is required for correct adjustRefNum() results + // as required for handling moved, deleted and edited CellRefs. + /// @note Does not validate. + void resolveParentFileIndices(const std::vector& files); const std::vector& getParentFileIndices() const { return mCtx.parentFileIndices; } + bool isValidParentFileIndex(int i) const { return i != getIndex(); } /************************************************************************* * @@ -279,7 +281,6 @@ private: Header mHeader; - std::vector *mGlobalReaderList; ToUTF8::Utf8Encoder* mEncoder; size_t mFileSize; diff --git a/components/esmloader/load.cpp b/components/esmloader/load.cpp index 789e7619b6..9879f33274 100644 --- a/components/esmloader/load.cpp +++ b/components/esmloader/load.cpp @@ -214,7 +214,6 @@ namespace EsmLoader ESM::ESMReader& reader = readers[i]; reader.setEncoder(encoder); reader.setIndex(static_cast(i)); - reader.setGlobalReaderList(&readers); reader.open(collection.getPath(file).string()); loadEsm(query, readers[i], result); From fac84b5dd36387e6f96325723be545c15ba5de72 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Tue, 2 Nov 2021 20:13:41 +0000 Subject: [PATCH 095/110] restores ESM::Dialogue order (#3209) With this PR we restore the previous order of `ESM::Dialogue` entries implicitly changed by PR #3197. In the future we may want to consider additional verification and documentation of `mShared` order inconsistencies. We might additionally consider applying this sorting in the particular code that requires it. --- apps/openmw/mwworld/store.cpp | 3 +++ apps/openmw/mwworld/store.hpp | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 06ea97a859..5e3f4079c3 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1005,6 +1005,9 @@ namespace MWWorld mShared.reserve(mStatic.size()); for (auto & [_, dial] : mStatic) mShared.push_back(&dial); + // TODO: verify and document this inconsistent behaviour + // TODO: if we require this behaviour, maybe we should move it to the place that requires it + std::sort(mShared.begin(), mShared.end(), [](const ESM::Dialogue* l, const ESM::Dialogue* r) -> bool { return l->mId < r->mId; }); } template <> diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index dbb28258ef..4c32b65ec7 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -8,7 +8,8 @@ #include #include -#include "recordcmp.hpp" +#include +#include namespace ESM { @@ -150,9 +151,11 @@ namespace MWWorld { typedef std::unordered_map Static; Static mStatic; - std::vector mShared; // Preserves the record order as it came from the content files (this - // is relevant for the spell autocalc code and selection order - // for heads/hairs in the character creation) + /// @par mShared usually preserves the record order as it came from the content files (this + /// is relevant for the spell autocalc code and selection order + /// for heads/hairs in the character creation) + /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons. + std::vector mShared; typedef std::unordered_map Dynamic; Dynamic mDynamic; From 2d3c6faec414b1f92729f4a1e1ef7bba919f4445 Mon Sep 17 00:00:00 2001 From: psi29a Date: Tue, 26 Oct 2021 08:59:11 +0000 Subject: [PATCH 096/110] Merge branch 'conditional_push_builds' into 'master' Add support for daily builds See merge request OpenMW/openmw!1314 (cherry picked from commit 50ea9869528c984b8ea66864fa08f5f710734eff) 1ee18b88 Update .gitlab-ci.yml file 603b0ad8 Update .gitlab-ci.yml a69cc468 Update .gitlab-ci.yml --- .gitlab-ci.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b69e930ae2..a07dec1d68 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,9 @@ stages: - docker - linux image: debian:bullseye + rules: + - if: $CI_PIPELINE_SOURCE == "push" + .Debian: extends: .Debian_Image @@ -52,7 +55,7 @@ Coverity: extends: .Debian_Image stage: build rules: - - if: '$CI_PIPELINE_SOURCE == "schedule"' + - if: $CI_PIPELINE_SOURCE == "schedule" before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity - curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN @@ -70,7 +73,6 @@ Coverity: variables: CC: gcc CXX: g++ - artifacts: Debian_GCC: extends: .Debian @@ -162,6 +164,7 @@ Debian_Clang_tests_Debug: only: variables: - $CI_PROJECT_ID == "7107382" + - $CI_PIPELINE_SOURCE == "push" cache: paths: - ccache/ @@ -213,6 +216,8 @@ variables: &tests-targets .Windows_Ninja_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -329,6 +334,8 @@ Windows_Ninja_Tests_RelWithDebInfo: .Windows_MSBuild_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -389,6 +396,15 @@ Windows_Ninja_Tests_RelWithDebInfo: - MSVC2019_64/*/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*/*.log +Daily_Windows_MSBuild_Engine_Release:on-schedule: + extends: + - .Windows_MSBuild_Base + variables: + <<: *engine-targets + config: "Release" + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + Windows_MSBuild_Engine_Release: extends: - .Windows_MSBuild_Base @@ -444,6 +460,8 @@ Debian_AndroidNDK_arm64-v8a: tags: - linux image: debian:bullseye + rules: + - if: $CI_PIPELINE_SOURCE == "push" variables: CCACHE_SIZE: 3G cache: From f684c1da528106abf59e0aacdea93552c59adb36 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 09:15:05 +0000 Subject: [PATCH 097/110] fixes assertion (#3215) This PR fixes an assertion introduced by #3211. For some reason my build originally did not contain assertions despite passing `DEBUG` into cmake, but after deleting `CMakeCache.txt` I have now hit the assertion @glassmancody reported as well. --- apps/openmw/mwworld/esmloader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index 647f5d3609..1917c41428 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -42,8 +42,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) ESM::ESMReader lEsm; lEsm.setEncoder(mEncoder); lEsm.setIndex(index); - lEsm.resolveParentFileIndices(mEsm); lEsm.open(filepath.string()); + lEsm.resolveParentFileIndices(mEsm); mEsm[index] = lEsm; mStore.load(mEsm[index], &mListener); } From db04dee29da240f8f671cef0d9a565c6a0e3abdd Mon Sep 17 00:00:00 2001 From: wareya Date: Thu, 4 Nov 2021 10:09:48 -0400 Subject: [PATCH 098/110] Force MSVC to build in utf-8 mode --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b4e5609ae..f2ee87b2ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,10 @@ if (WIN32) option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) endif() +if(MSVC) + add_compile_options("/utf-8") +endif() + # Dependencies find_package(OpenGL REQUIRED) From 4c3dd1a964eca65f1b68b15abd2877ba64768e4e Mon Sep 17 00:00:00 2001 From: "glassmancody.info" Date: Thu, 4 Nov 2021 08:42:22 -0700 Subject: [PATCH 099/110] remove particle node when resetting particle effect --- apps/openmw/mwrender/sky.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index eb7c4a3882..b6b0a3669b 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -535,7 +535,11 @@ namespace MWRender mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); if (!enabled && mParticleNode && mParticleEffect) - mCurrentParticleEffect = {}; + { + mCurrentParticleEffect.clear(); + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } mEnabled = enabled; } From 3042c000c6d51cc9ed819325ccc8cc0622c8478f Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 15:53:57 +0000 Subject: [PATCH 100/110] fixes setActorFade logic errors (#3195) * resets state updater to apply light settings (#3141) resets state updater to apply light settings With this PR we achieve the same effect with fewer lines of code. * fixes LightSource logic errors We currently update `LightSource::setActorFade` in `TransparencyUpdater`. There are several logic errors inherent in this approach: 1. We fail to update `LightSource::setActorFade` for off screen actors because their `TransparencyUpdater` cull callback is not invoked. 2. We fail to update `LightSource::setActorFade` in the instant that a `TransparencyUpdater` is removed. 3. We fail to update `setActorFade` when an `mExtraLightSource` is created after calling `Animation::setAlpha`. With this PR we avoid such issues by updating `LightSource::setActorFade` in `Animation::setAlpha` and `Animation::addExtraLightSource` instead. --- apps/openmw/mwrender/animation.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 91ef6cc975..ecfe65c575 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -389,11 +389,6 @@ namespace MWRender mAlpha = alpha; } - void setLightSource(const osg::ref_ptr& lightSource) - { - mLightSource = lightSource; - } - protected: void setDefaults(osg::StateSet* stateset) override { @@ -416,13 +411,10 @@ namespace MWRender { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); - if (mLightSource) - mLightSource->setActorFade(mAlpha); } private: float mAlpha; - osg::ref_ptr mLightSource; }; struct Animation::AnimSource @@ -1485,6 +1477,7 @@ namespace MWRender bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource->setActorFade(mAlpha); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1639,7 +1632,6 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); - mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else @@ -1650,6 +1642,8 @@ namespace MWRender mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } + if (mExtraLightSource) + mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) From 1979ee1491a2e8065a00e998b2b357445528c6c4 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 15:54:47 +0000 Subject: [PATCH 101/110] refactors hashed std::map (#3199) We currently apply a strange algorithm to `LightManager::mStateSetCache`. For some reason this algorithm inserts hashed keys into `std::map` in a way that fails to handle hash collisions and exhibits worse lookup complexity than `std::unordered_map`. With this PR we just use `std::unordered_map` here. --- components/sceneutil/lightmanager.cpp | 42 +++++++++++++++++---------- components/sceneutil/lightmanager.hpp | 8 +++-- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index f38fd80d26..a5ce448b75 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,6 +1,8 @@ #include "lightmanager.hpp" #include +#include +#include #include #include @@ -1158,29 +1160,39 @@ namespace SceneUtil return mSun; } + size_t LightManager::HashLightIdList::operator()(const LightIdList& lightIdList) const + { + size_t hash = 0; + for (size_t i = 0; i < lightIdList.size(); ++i) + Misc::hashCombine(hash, lightIdList[i]); + return hash; + } + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) { // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - size_t hash = 0; - for (size_t i = 0; i < lightList.size(); ++i) + + if (getLightingMethod() == LightingMethod::SingleUBO) { - auto id = lightList[i]->mLightSource->getId(); - Misc::hashCombine(hash, id); + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; - if (getLightingMethod() != LightingMethod::SingleUBO) - continue; - - if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) - continue; - - int index = getLightIndexMap(frameNum).size() + 1; - updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); - getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(id, index); + } } auto& stateSetCache = mStateSetCache[frameNum%2]; - auto found = stateSetCache.find(hash); + LightIdList lightIdList; + lightIdList.reserve(lightList.size()); + std::transform(lightList.begin(), lightList.end(), std::back_inserter(lightIdList), [] (const LightSourceViewBound* l) { return l->mLightSource->getId(); }); + + auto found = stateSetCache.find(lightIdList); if (found != stateSetCache.end()) { mStateSetGenerator->update(found->second, lightList, frameNum); @@ -1188,7 +1200,7 @@ namespace SceneUtil } auto stateset = mStateSetGenerator->generate(lightList, frameNum); - stateSetCache.emplace(hash, stateset); + stateSetCache.emplace(lightIdList, stateset); return stateset; } diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index b518a4723c..4a7dc7dbe7 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -207,8 +207,12 @@ namespace SceneUtil using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; - // < Light list hash , StateSet > - using LightStateSetMap = std::map>; + using LightIdList = std::vector; + struct HashLightIdList + { + size_t operator()(const LightIdList&) const; + }; + using LightStateSetMap = std::unordered_map, HashLightIdList>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; From 6cf74f7041c120f25e27d54b579ee19bd62bc2a5 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 15:55:32 +0000 Subject: [PATCH 102/110] refactors ESM::Land (#3213) With this PR we reduce coupling, simplify code, encapsulate a variable and separate actual `ESM` data from its context. --- apps/opencs/model/world/collection.hpp | 4 ++-- apps/opencs/model/world/columnimp.cpp | 2 +- apps/openmw/mwworld/esmstore.cpp | 2 +- apps/openmw/mwworld/store.cpp | 15 ++------------- apps/openmw/mwworld/store.hpp | 5 ++--- components/esm/loadland.cpp | 8 ++------ components/esm/loadland.hpp | 5 ++++- components/esmterrain/storage.hpp | 6 +----- 8 files changed, 15 insertions(+), 32 deletions(-) diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 6ab9d7ff9d..d15a4ff54d 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -296,7 +296,7 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type) { int index = cloneRecordImp(origin, destination, type); - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); } template @@ -311,7 +311,7 @@ namespace CSMWorld int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); return true; } diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index bec5008d35..0244ab1e8d 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -52,7 +52,7 @@ namespace CSMWorld QVariant LandPluginIndexColumn::get(const Record& record) const { - return record.get().mPlugin; + return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 183b13ca09..bafdc8f37d 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -153,7 +153,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) // Land texture loading needs to use a separate internal store for each plugin. // We set the number of plugins here so we can properly verify if valid plugin // indices are being passed to the LandTexture Store retrieval methods. - mLandTextures.resize(mLandTextures.getSize()+1); + mLandTextures.addPlugin(); // Loop through all records while(esm.hasMoreRecs()) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 5e3f4079c3..4d720a11a7 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -322,15 +322,13 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].size(); } - RecordId Store::load(ESM::ESMReader &esm, size_t plugin) + RecordId Store::load(ESM::ESMReader &esm) { ESM::LandTexture lt; bool isDeleted = false; lt.load(esm, isDeleted); - assert(plugin < mStatic.size()); - // Replace texture for records with given ID and index from all plugins. for (unsigned int i=0; i (int)ltexl.size()) ltexl.resize(lt.mIndex+1); @@ -352,10 +350,6 @@ namespace MWWorld return RecordId(ltexl[idx].mId, isDeleted); } - RecordId Store::load(ESM::ESMReader &esm) - { - return load(esm, esm.getIndex()); - } Store::iterator Store::begin(size_t plugin) const { assert(plugin < mStatic.size()); @@ -366,11 +360,6 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].end(); } - void Store::resize(size_t num) - { - if (mStatic.size() < num) - mStatic.resize(num); - } // Land //========================================================================= diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 4c32b65ec7..17a37c23ea 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -222,13 +222,12 @@ namespace MWWorld const ESM::LandTexture *search(size_t index, size_t plugin) const; const ESM::LandTexture *find(size_t index, size_t plugin) const; - /// Resize the internal store to hold at least \a num plugins. - void resize(size_t num); + /// Resize the internal store to hold another plugin. + void addPlugin() { mStatic.emplace_back(); } size_t getSize() const override; size_t getSize(size_t plugin) const; - RecordId load(ESM::ESMReader &esm, size_t plugin); RecordId load(ESM::ESMReader &esm) override; iterator begin(size_t plugin) const; diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index d7dcd47c69..3c4a1e0b07 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -15,7 +15,6 @@ namespace ESM : mFlags(0) , mX(0) , mY(0) - , mPlugin(0) , mDataTypes(0) , mLandData(nullptr) { @@ -40,8 +39,6 @@ namespace ESM { isDeleted = false; - mPlugin = esm.getIndex(); - bool hasLocation = false; bool isLoaded = false; while (!isLoaded && esm.hasMoreSubs()) @@ -192,7 +189,7 @@ namespace ESM void Land::blank() { - mPlugin = 0; + setPlugin(0); std::fill(std::begin(mWnam), std::end(mWnam), 0); @@ -326,7 +323,7 @@ namespace ESM } Land::Land (const Land& land) - : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mPlugin (land.mPlugin), + : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mContext (land.mContext), mDataTypes (land.mDataTypes), mLandData (land.mLandData ? new LandData (*land.mLandData) : nullptr) { @@ -345,7 +342,6 @@ namespace ESM std::swap (mFlags, land.mFlags); std::swap (mX, land.mX); std::swap (mY, land.mY); - std::swap (mPlugin, land.mPlugin); std::swap (mContext, land.mContext); std::swap (mDataTypes, land.mDataTypes); std::swap (mLandData, land.mLandData); diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 67dd2e76a2..610dd28fb8 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -29,7 +29,10 @@ struct Land int mFlags; // Only first four bits seem to be used, don't know what // they mean. int mX, mY; // Map coordinates. - int mPlugin; // Plugin index, used to reference the correct material palette. + + // Plugin index, used to reference the correct material palette. + int getPlugin() const { return mContext.index; } + void setPlugin(int index) { mContext.index = index; } // File context. This allows the ESM reader to be 'reset' to this // location later when we are ready to load the full data set. diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 68e71574ee..107255a7af 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -36,11 +36,7 @@ namespace ESMTerrain return nullptr; return &mData; } - - inline int getPlugin() const - { - return mLand->mPlugin; - } + inline int getPlugin() const { return mLand->getPlugin(); } private: const ESM::Land* mLand; From cd946ea35a7f5e8d8ecfa873bea637d679d40c1c Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 15:56:51 +0000 Subject: [PATCH 103/110] removes unused recordcmp.hpp (#3214) This PR removes an unused source file. --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwworld/recordcmp.hpp | 34 ------------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 apps/openmw/mwworld/recordcmp.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index fe402cd0b7..f22204eb8c 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -72,7 +72,7 @@ add_openmw_dir (mwworld containerstore actiontalk actiontake manualref player cellvisitors failedaction cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat - store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor + store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager ) diff --git a/apps/openmw/mwworld/recordcmp.hpp b/apps/openmw/mwworld/recordcmp.hpp deleted file mode 100644 index f749351cea..0000000000 --- a/apps/openmw/mwworld/recordcmp.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWWORLD_RECORDCMP_H -#define OPENMW_MWWORLD_RECORDCMP_H - -#include - -#include - -namespace MWWorld -{ - struct RecordCmp - { - template - bool operator()(const T &x, const T& y) const { - return x.mId < y.mId; - } - }; - - template <> - inline bool RecordCmp::operator()(const ESM::Dialogue &x, const ESM::Dialogue &y) const { - return Misc::StringUtils::ciLess(x.mId, y.mId); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Cell &x, const ESM::Cell &y) const { - return Misc::StringUtils::ciLess(x.mName, y.mName); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - return Misc::StringUtils::ciLess(x.mCell, y.mCell); - } - -} // end namespace -#endif From 03710726315f1e649db747cad37180596fdeb0fc Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 21:20:06 +0000 Subject: [PATCH 104/110] removes lowerCaseInPlace (#3217) This PR removes unneeded `lowerCaseInPlace` calls in in a hot path of `objectpaging.cpp` that are no longer necessary after PR #3197. In addition, I have been informed that these changes should by coincidence address a compiler specific compilation error we currently experience. --- apps/openmw/mwrender/objectpaging.cpp | 2 -- components/misc/resourcehelpers.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 947f23f781..b6f12dea37 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -428,7 +428,6 @@ namespace MWRender continue; if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; if (deleted) { refs.erase(ref.mRefNum); continue; } @@ -444,7 +443,6 @@ namespace MWRender for (auto [ref, deleted] : cell->mLeasedRefs) { if (deleted) { refs.erase(ref.mRefNum); continue; } - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; refs[ref.mRefNum] = std::move(ref); diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 4d54baafc6..610c7a790c 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -142,5 +142,5 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id) { - return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; + return Misc::StringUtils::ciEqual(id, "prisonmarker") || Misc::StringUtils::ciEqual(id, "divinemarker") || Misc::StringUtils::ciEqual(id, "templemarker") || Misc::StringUtils::ciEqual(id, "northmarker"); } From 5debd6e25af486d8234b9ece7d4270b9c63ee099 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Thu, 4 Nov 2021 21:31:22 +0000 Subject: [PATCH 105/110] removes two dummy serialisers (#3212) This PR removes dummy serialisers for `StateSetUpdater`, `NodeCallback` and the respective `META` macros that trigger serialisation requirement here. `StateSetUpdater` and `NodeCallback` are just base classes that can not be used on their own, so there is no need to incorporate them into serialisation. These changes might have minor effects on derived classes that forget to override `className()`, `libraryName()` through `META`, but it makes hardly a difference to now serialise such classes as a dysfunctional `osg::Callback` instead of a dysfunctional `SceneUtil::NodeCallback`. --- components/sceneutil/nodecallback.hpp | 1 - components/sceneutil/serialize.cpp | 2 -- components/sceneutil/statesetupdater.hpp | 2 -- 3 files changed, 5 deletions(-) diff --git a/components/sceneutil/nodecallback.hpp b/components/sceneutil/nodecallback.hpp index 942cb17ded..96e3ae229e 100644 --- a/components/sceneutil/nodecallback.hpp +++ b/components/sceneutil/nodecallback.hpp @@ -19,7 +19,6 @@ public: NodeCallback(){} NodeCallback(const NodeCallback& nc,const osg::CopyOp& copyop): osg::Callback(nc, copyop) {} - META_Object(SceneUtil, NodeCallback) bool run(osg::Object* object, osg::Object* data) override { diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 134f7c29dd..9da0d6a40e 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -125,11 +125,9 @@ void registerSerializers() "SceneUtil::CompositeStateSetUpdater", "SceneUtil::LightListCallback", "SceneUtil::LightManagerUpdateCallback", - "SceneUtil::NodeCallback", "SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource", - "SceneUtil::StateSetUpdater", "SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder", diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 35be9cb434..cc2e248457 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -34,8 +34,6 @@ namespace SceneUtil StateSetUpdater(); StateSetUpdater(const StateSetUpdater& copy, const osg::CopyOp& copyop); - META_Object(SceneUtil, StateSetUpdater) - void operator()(osg::Node* node, osg::NodeVisitor* nv); /// Apply state - to override in derived classes From d5aaa0394ab1a30d8f2ccfb14f5a61d7966bf694 Mon Sep 17 00:00:00 2001 From: wareya Date: Thu, 4 Nov 2021 10:44:28 -0400 Subject: [PATCH 106/110] Fix a missing required include --- apps/openmw/mwworld/ptr.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 438b87c7af..4dbdfa5545 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "livecellref.hpp" From ae84a0c9d5a68c6a7fa30c8fa4701ecc6308edd5 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Thu, 4 Nov 2021 23:21:58 +0100 Subject: [PATCH 107/110] Fix most of reStructuredText warnings --- .../manuals/installation/install-game-files.rst | 8 ++++---- docs/source/reference/documentationHowTo.rst | 6 +++--- docs/source/reference/modding/settings/game.rst | 2 +- docs/source/reference/modding/settings/general.rst | 2 +- .../reference/modding/settings/groundcover.rst | 2 ++ docs/source/reference/modding/settings/map.rst | 1 + docs/source/reference/modding/settings/navigator.rst | 10 +++++----- docs/source/reference/modding/settings/shaders.rst | 2 +- .../texture-modding/convert-bump-mapped-mods.rst | 12 ++++++------ 9 files changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 538cfd4c6d..2af7946536 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -105,10 +105,10 @@ If you are running macOS, you can also download Morrowind through Steam: #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` -Linux ----- -Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". ----- +Linux +----- +Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". +--------------------------------------------------------- #. Install Steam from "Ubuntu Software" Center #. Enable Proton (basically WINE under the hood). This is done in the Steam client menu drop down. Select, "Steam | Settings" then in the "SteamPlay" section check the box next to "enable steam play for all other titles" #. Now Morrowind should be selectable in your game list (as long as you own it). You can install it like any other game, choose to install it and remember the directory path of the location you pick. diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 75dbe8dca2..d2b67d02ca 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -154,9 +154,9 @@ A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" - and merge the changes you've made into the project master repository. One of the project maintainers will probably ask - you to make some corrections or clarifications. Go back and repeat this process to make those changes, - and repeat until they're good enough to get merged. +and merge the changes you've made into the project master repository. One of the project maintainers will probably ask +you to make some corrections or clarifications. Go back and repeat this process to make those changes, +and repeat until they're good enough to get merged. So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index fb7b537701..58d5345f65 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -441,7 +441,7 @@ Some mods add harvestable container models. When this setting is enabled, activa When this setting is turned off or when activating a regular container, the menu will open as usual. allow actors to follow over water surface ---------------------- +----------------------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/general.rst b/docs/source/reference/modding/settings/general.rst index f0ebe4f972..ee5b908b4a 100644 --- a/docs/source/reference/modding/settings/general.rst +++ b/docs/source/reference/modding/settings/general.rst @@ -61,7 +61,7 @@ Mipmapping is a way of reducing the processing power needed during minification by pregenerating a series of smaller textures. notify on saved screenshot --------------- +-------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index 3e943e4284..7b060f58ad 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -51,6 +51,7 @@ Determines whether grass should respond to the player treading on it. .. list-table:: Modes :header-rows: 1 + * - Mode number - Meaning * - 0 @@ -77,6 +78,7 @@ How far away from the player grass can be before it's unaffected by being trod o .. list-table:: Presets :header-rows: 1 + * - Preset number - Range (Units) - Distance (Units) diff --git a/docs/source/reference/modding/settings/map.rst b/docs/source/reference/modding/settings/map.rst index a4d3cd7e0d..1412d6584f 100644 --- a/docs/source/reference/modding/settings/map.rst +++ b/docs/source/reference/modding/settings/map.rst @@ -125,6 +125,7 @@ max local viewing distance This setting controls the viewing distance on local map when 'distant terrain' is enabled. If this setting is greater than the viewing distance then only up to the viewing distance is used for local map, otherwise the viewing distance is used. If view distance is changed in settings menu during the game, then viewable distance on the local map is not updated. + .. warning:: Increasing this setting can increase cell load times, because the localmap take a snapshot of each cell contained in a square of 2 x (max local viewing distance) + 1 square. diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index fee4b2626e..aea817530e 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -1,5 +1,5 @@ Navigator Settings -################ +################## Main settings ************* @@ -43,7 +43,7 @@ Increasing this value may decrease performance. It's a limitation of `Recastnavigation `_ library. wait until min distance to player ------------------------------- +--------------------------------- :Type: integer :Range: >= 0 @@ -87,7 +87,7 @@ Memory will be consumed in approximately linear dependency from number of nav me But only for new locations or already dropped from cache. min update interval ms ----------------- +---------------------- :Type: integer :Range: >= 0 @@ -181,7 +181,7 @@ Every nav mesh is visible and every update is noticable. Potentially decreases performance. enable agents paths render -------------------- +-------------------------- :Type: boolean :Range: True/False @@ -193,7 +193,7 @@ Works even if Navigator is disabled. Potentially decreases performance. enable recast mesh render ----------------------- +------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index 03b7805de6..296a351435 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -241,7 +241,7 @@ lighting` is on. This setting has no effect if :ref:`lighting method` is 'legacy'. minimum interior brightness ------------------------- +--------------------------- :Type: float :Range: 0.0-1.0 diff --git a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst index 0ad35d7a50..e05177c268 100644 --- a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst +++ b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst @@ -15,7 +15,7 @@ Normal maps from Morrowind to OpenMW - `Tutorial - Morrowind, Part 2`_ General introduction to normal map conversion ------------------------------------------------- +--------------------------------------------- :Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov :Updated: 2020-03-03 @@ -34,7 +34,7 @@ There are several techniques for bump-mapping, and normal-mapping is the most co So let's get on with it. OpenMW normal-mapping -************************ +********************* Normal-mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix, and you're done. @@ -70,7 +70,7 @@ settings.cfg_-file. Add these rows where it would make sense: See OpenMW's wiki page about `texture modding`_ to read more about it. Morrowind bump-mapping -***************************************************** +********************** **Conversion difficulty:** *Varies. Sometimes quick and easy, sometimes time-consuming and hard.* @@ -93,7 +93,7 @@ In this case you can benefit from OpenMW's normal-mapping support by using these This means that you will have to drop the bump-mapping references from the model and sometimes rename the texture. MGE XE normal-mapping -*************************************** +********************* **Conversion difficulty:** *Easy* @@ -169,7 +169,7 @@ depending on a few circumstances. In this tutorial, we will look at a very easy, although in some cases a bit time-consuming, example. Tutorial - Morrowind, Part 1 -********************** +**************************** We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_. Since this is just a texture pack and not a model replacer, @@ -201,7 +201,7 @@ We ignored those model files since they are not needed with OpenMW. In this tuto we will convert a mod that includes new, custom-made models. In other words, we cannot just ignore those files this time. Tutorial - Morrowind, Part 2 -********************** +**************************** The sacks included in Apel's `Various Things - Sacks`_ come in two versions – without bump-mapping, and with bump-mapping. Since we want the glory of normal-mapping in our OpenMW setup, we will go with the bump-mapped version. From 1960e976e2640603f7f21af1b46d6ce65af50a52 Mon Sep 17 00:00:00 2001 From: Bo Svensson <90132211+bosvensson1@users.noreply.github.com> Date: Fri, 5 Nov 2021 09:53:52 +0000 Subject: [PATCH 108/110] refactors stringops.hpp (#3192) With this PR we refactor `StringUtils::replaceAll` to accept `string_view` as suggested in a code comment. In addition, while we are touching this rebuild happy file, we slim it down a bit by moving a few sparingly used functions elsewhere. --- apps/openmw/mwgui/journalviewmodel.cpp | 4 +- apps/openmw/mwgui/sortfilteritemmodel.cpp | 15 ++- apps/openmw/mwgui/spellmodel.cpp | 9 +- apps/openmw/mwlua/actions.cpp | 2 + .../openmw_test_suite/misc/test_stringops.cpp | 3 +- components/files/escape.cpp | 4 +- components/misc/algorithm.hpp | 26 ++++ components/misc/stringops.hpp | 123 +----------------- components/misc/utf8stream.hpp | 65 +++++++++ components/resource/scenemanager.cpp | 3 +- components/sceneutil/lightmanager.cpp | 1 + 11 files changed, 119 insertions(+), 136 deletions(-) diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 6b38cd0d9d..fc3fcc3efe 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -313,9 +313,9 @@ struct JournalViewModelImpl : JournalViewModel for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); - Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); + Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); - if (first != Misc::StringUtils::toLowerUtf8(character)) + if (first != Utf8Stream::toLowerUtf8(character)) continue; visitor (i->second.getName()); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index eb7ebd0e43..9d6ed49d3d 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,6 +1,7 @@ #include "sortfilteritemmodel.hpp" #include +#include #include #include #include @@ -69,8 +70,8 @@ namespace return compareType(leftType, rightType); // compare items by name - std::string leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); - std::string rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); + std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) @@ -213,7 +214,7 @@ namespace MWGui if (!mNameFilter.empty()) { - const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); + const auto itemName = Utf8Stream::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } @@ -226,7 +227,7 @@ namespace MWGui for (const auto& effect : effects) { - const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); + const auto ciEffect = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; @@ -285,7 +286,7 @@ namespace MWGui return false; } - std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if(compare.find(mNameFilter) == std::string::npos) return false; @@ -318,12 +319,12 @@ namespace MWGui void SortFilterItemModel::setNameFilter (const std::string& filter) { - mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mNameFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter (const std::string& filter) { - mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::update() diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 61ea9ce93a..455f167415 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -1,6 +1,7 @@ #include "spellmodel.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -69,7 +70,7 @@ namespace MWGui fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); } - std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); + std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; @@ -90,14 +91,14 @@ namespace MWGui const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); - std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); for (const ESM::Spell* spell : spells) { if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); + std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) @@ -139,7 +140,7 @@ namespace MWGui if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, enchant->mEffects)) diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index aad70d1a57..92a7f915bb 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -1,5 +1,7 @@ #include "actions.hpp" +#include + #include #include "../mwworld/cellstore.hpp" diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/openmw_test_suite/misc/test_stringops.cpp index 173cfa4447..7d8e93dc28 100644 --- a/apps/openmw_test_suite/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/misc/test_stringops.cpp @@ -1,5 +1,6 @@ #include #include "components/misc/stringops.hpp" +#include "components/misc/algorithm.hpp" #include #include @@ -18,7 +19,7 @@ struct PartialBinarySearchTest : public ::testing::Test bool matches(const std::string& keyword) { - return Misc::StringUtils::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); + return Misc::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); } }; diff --git a/components/files/escape.cpp b/components/files/escape.cpp index 8b11504d34..fcbcc04a16 100644 --- a/components/files/escape.cpp +++ b/components/files/escape.cpp @@ -29,10 +29,10 @@ namespace Files std::string temp = str; static const char hash[] = { escape_hash_filter::sEscape, escape_hash_filter::sHashIdentifier }; - Misc::StringUtils::replaceAll(temp, hash, "#", 2, 1); + Misc::StringUtils::replaceAll(temp, std::string_view(hash, 2), "#"); static const char escape[] = { escape_hash_filter::sEscape, escape_hash_filter::sEscapeIdentifier }; - Misc::StringUtils::replaceAll(temp, escape, "@", 2, 1); + Misc::StringUtils::replaceAll(temp, std::string_view(escape, 2), "@"); return temp; } diff --git a/components/misc/algorithm.hpp b/components/misc/algorithm.hpp index 4d70afa86c..54ac74e97e 100644 --- a/components/misc/algorithm.hpp +++ b/components/misc/algorithm.hpp @@ -4,6 +4,8 @@ #include #include +#include "stringops.hpp" + namespace Misc { template @@ -31,6 +33,30 @@ namespace Misc } return begin; } + + /// Performs a binary search on a sorted container for a string that 'key' starts with + template + static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + { + const Iterator notFound = end; + + while(begin < end) + { + const Iterator middle = begin + (std::distance(begin, end) / 2); + + int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); + + if(comp == 0) + return middle; + else if(comp > 0) + end = middle; + else + begin = middle + 1; + } + + return notFound; + } + } #endif diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index dcffa7fdf3..12633db826 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -8,8 +8,6 @@ #include #include -#include "utf8stream.hpp" - namespace Misc { class StringUtils @@ -45,70 +43,6 @@ public: return (c >= 'A' && c <= 'Z') ? c + 'a' - 'A' : c; } - static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) - { - // Russian alphabet - if (ch >= 0x0410 && ch < 0x0430) - return ch + 0x20; - - // Cyrillic IO character - if (ch == 0x0401) - return ch + 0x50; - - // Latin alphabet - if (ch >= 0x41 && ch < 0x60) - return ch + 0x20; - - // Deutch characters - if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) - return ch + 0x20; - if (ch == 0x1e9e) - return 0xdf; - - // TODO: probably we will need to support characters from other languages - - return ch; - } - - static std::string lowerCaseUtf8(const std::string& str) - { - if (str.empty()) - return str; - - // Decode string as utf8 characters, convert to lower case and pack them to string - std::string out; - Utf8Stream stream (str.c_str()); - while (!stream.eof ()) - { - Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); - - if (character <= 0x7f) - out.append(1, static_cast(character)); - else if (character <= 0x7ff) - { - out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else if (character <= 0xffff) - { - out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else - { - out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); - out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - - stream.consume(); - } - - return out; - } - static bool ciLess(const std::string &x, const std::string &y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } @@ -207,55 +141,21 @@ public: } }; - - /// Performs a binary search on a sorted container for a string that 'key' starts with - template - static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) - { - const Iterator notFound = end; - - while(begin < end) - { - const Iterator middle = begin + (std::distance(begin, end) / 2); - - int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); - - if(comp == 0) - return middle; - else if(comp > 0) - end = middle; - else - begin = middle + 1; - } - - return notFound; - } - /** @brief Replaces all occurrences of a string in another string. * * @param str The string to operate on. * @param what The string to replace. * @param with The replacement string. - * @param whatLen The length of the string to replace. - * @param withLen The length of the replacement string. - * * @return A reference to the string passed in @p str. */ - static std::string &replaceAll(std::string &str, const char *what, const char *with, - std::size_t whatLen=std::string::npos, std::size_t withLen=std::string::npos) + static std::string &replaceAll(std::string &str, std::string_view what, std::string_view with) { - if (whatLen == std::string::npos) - whatLen = strlen(what); - - if (withLen == std::string::npos) - withLen = strlen(with); - std::size_t found; std::size_t offset = 0; - while((found = str.find(what, offset, whatLen)) != std::string::npos) + while((found = str.find(what, offset)) != std::string::npos) { - str.replace(found, whatLen, with, withLen); - offset = found + withLen; + str.replace(found, what.size(), with); + offset = found + with.size(); } return str; } @@ -311,26 +211,11 @@ public: cont.push_back(str.substr(previous, current - previous)); } - // TODO: use the std::string_view once we will use the C++17. - // It should allow us to avoid data copying while we still will support both string and literal arguments. - - static inline void replaceAll(std::string& data, const std::string& toSearch, const std::string& replaceStr) - { - size_t pos = data.find(toSearch); - - while( pos != std::string::npos) - { - data.replace(pos, toSearch.size(), replaceStr); - pos = data.find(toSearch, pos + replaceStr.size()); - } - } - static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) { size_t pos = str.rfind(substr); if (pos == std::string::npos) return; - str.replace(pos, substr.size(), with); } diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index e499d15e60..9dc8aa8208 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -2,6 +2,7 @@ #define MISC_UTF8ITER_HPP #include +#include #include class Utf8Stream @@ -87,6 +88,70 @@ public: return std::make_pair (chr, cur); } + static UnicodeChar toLowerUtf8(UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch + 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch + 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch + 0x20; + + // German characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch + 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string& str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + private: static std::pair octet_count (unsigned char octet) diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 4184a77c54..c1d567c0e1 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -20,6 +20,7 @@ #include #include +#include #include @@ -551,7 +552,7 @@ namespace Resource std::sort(reservedNames.begin(), reservedNames.end(), Misc::StringUtils::ciLess); } - std::vector::iterator it = Misc::StringUtils::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); + std::vector::iterator it = Misc::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); return it != reservedNames.end(); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index a5ce448b75..448f6ca916 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,6 +1,7 @@ #include "lightmanager.hpp" #include +#include #include #include From 3f80725ebe333ef4bb847b3fe0c173ad85c1a186 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 5 Nov 2021 22:54:50 +0100 Subject: [PATCH 109/110] Remove duplicated implementation of Misc::Convert::toOsg --- apps/openmw/mwworld/scene.cpp | 4 ++-- components/detournavigator/gettilespositions.hpp | 6 ++++-- components/detournavigator/recastmeshbuilder.cpp | 2 +- components/misc/convert.hpp | 5 ----- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index f9e792c7d2..769817810a 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -147,12 +147,12 @@ namespace transform.getOrigin() ); - const auto start = Misc::Convert::makeOsgVec3f(closedDoorTransform(center + toPoint)); + const auto start = Misc::Convert::toOsg(closedDoorTransform(center + toPoint)); const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start; - const auto end = Misc::Convert::makeOsgVec3f(closedDoorTransform(center - toPoint)); + const auto end = Misc::Convert::toOsg(closedDoorTransform(center - toPoint)); const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end; diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index 27c8f7a4ac..e8ba8beba9 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -46,13 +46,15 @@ namespace DetourNavigator btVector3 aabbMax; shape.getAabb(transform, aabbMin, aabbMax); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(Misc::Convert::toOsg(aabbMin), Misc::Convert::toOsg(aabbMax), settings, std::forward(callback)); } template void getTilesPositions(const int cellSize, const osg::Vec3f& shift, const Settings& settings, Callback&& callback) { + using Misc::Convert::toOsg; + const auto halfCellSize = cellSize / 2; const btTransform transform(btMatrix3x3::getIdentity(), Misc::Convert::toBullet(shift)); auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); @@ -64,7 +66,7 @@ namespace DetourNavigator aabbMax.setX(std::max(aabbMin.x(), aabbMax.x())); aabbMax.setY(std::max(aabbMin.y(), aabbMax.y())); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(toOsg(aabbMin), toOsg(aabbMax), settings, std::forward(callback)); } } diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 73b731c247..8f860d2eb1 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -30,7 +30,7 @@ namespace DetourNavigator RecastMeshTriangle result; result.mAreaType = areaType; for (std::size_t i = 0; i < 3; ++i) - result.mVertices[i] = Misc::Convert::makeOsgVec3f(vertices[i]); + result.mVertices[i] = Misc::Convert::toOsg(vertices[i]); return result; } diff --git a/components/misc/convert.hpp b/components/misc/convert.hpp index 81270c0c0b..6f4a55cfcc 100644 --- a/components/misc/convert.hpp +++ b/components/misc/convert.hpp @@ -19,11 +19,6 @@ namespace Convert return osg::Vec3f(values[0], values[1], values[2]); } - inline osg::Vec3f makeOsgVec3f(const btVector3& value) - { - return osg::Vec3f(value.x(), value.y(), value.z()); - } - inline osg::Vec3f makeOsgVec3f(const ESM::Pathgrid::Point& value) { return osg::Vec3f(value.mX, value.mY, value.mZ); From 7a0c13fcf87d5916f72b471b41ca833ebf05c811 Mon Sep 17 00:00:00 2001 From: Alexei Dobrohotov Date: Sat, 6 Nov 2021 07:30:28 +0300 Subject: [PATCH 110/110] Make better use of std::clamp --- apps/opencs/view/render/object.cpp | 2 +- apps/openmw/mwdialogue/dialoguemanagerimp.cpp | 2 +- apps/openmw/mwgui/dialogue.cpp | 3 +-- apps/openmw/mwgui/enchantingdialog.cpp | 2 +- apps/openmw/mwgui/hud.cpp | 2 +- apps/openmw/mwgui/keyboardnavigation.cpp | 2 +- apps/openmw/mwgui/settingswindow.cpp | 4 ++-- apps/openmw/mwgui/statswindow.cpp | 3 +-- apps/openmw/mwinput/controllermanager.cpp | 2 +- apps/openmw/mwinput/mousemanager.cpp | 4 ++-- apps/openmw/mwmechanics/actors.cpp | 11 ++++------- apps/openmw/mwmechanics/character.cpp | 12 ++++++------ apps/openmw/mwmechanics/combat.cpp | 7 +++---- apps/openmw/mwmechanics/difficultyscaling.cpp | 4 +--- apps/openmw/mwmechanics/enchanting.cpp | 6 +++--- .../openmw/mwmechanics/mechanicsmanagerimp.cpp | 18 +++++++++--------- apps/openmw/mwmechanics/npcstats.cpp | 2 +- apps/openmw/mwmechanics/spelleffects.cpp | 2 +- apps/openmw/mwmechanics/spellutil.cpp | 5 ++++- apps/openmw/mwmechanics/weaponpriority.cpp | 3 +-- .../mwphysics/hasspherecollisioncallback.hpp | 6 +++--- apps/openmw/mwphysics/physicssystem.cpp | 2 +- apps/openmw/mwrender/camera.cpp | 6 +++--- apps/openmw/mwrender/localmap.cpp | 6 +++--- apps/openmw/mwrender/objectpaging.cpp | 8 ++------ apps/openmw/mwrender/renderingmanager.cpp | 5 ++--- apps/openmw/mwrender/water.cpp | 9 ++++----- apps/openmw/mwscript/aiextensions.cpp | 3 +-- apps/openmw/mwscript/skyextensions.cpp | 2 +- apps/openmw/mwsound/loudness.cpp | 5 ++--- apps/openmw/mwsound/volumesettings.cpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 4 ++-- components/esm/loadland.cpp | 2 +- components/fontloader/fontloader.cpp | 9 +++------ components/nifosg/controller.cpp | 2 +- components/sceneutil/shadow.cpp | 8 +++----- components/widgets/fontwrapper.hpp | 6 +----- components/widgets/numericeditbox.cpp | 2 +- 38 files changed, 80 insertions(+), 103 deletions(-) diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 789fad0587..9f4fd29966 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -308,7 +308,7 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) const float OuterRadius = InnerRadius + MarkerShaftWidth; const float SegmentDistance = 100.f; - const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance))); + const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 8aa6acd8ed..5270c0143a 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -421,7 +421,7 @@ namespace MWDialogue // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) npcStats.setBaseDisposition(0); int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); - int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero)); + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); npcStats.setBaseDisposition(disposition); } diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 1da77f5d06..86cad0fa7a 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -347,8 +347,7 @@ namespace MWGui { if (!mScrollBar->getVisible()) return; - mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), - std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); + mScrollBar->setScrollPosition(std::clamp(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index d0d2118c6e..ee0067f082 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -109,7 +109,7 @@ namespace MWGui { mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); - mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance())))); + mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 5a7b5a9590..ab596137cd 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -610,7 +610,7 @@ namespace MWGui static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) - mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); + mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index b718b712c0..3220e16b94 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -273,7 +273,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap) if (wrap) index = (index + keyFocusList.size())%keyFocusList.size(); else - index = std::min(std::max(0, index), static_cast(keyFocusList.size())-1); + index = std::clamp(index, 0, keyFocusList.size() - 1); MyGUI::Widget* next = keyFocusList[index]; int vertdiff = next->getTop() - focus->getTop(); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 3b4afc852f..257c129e8c 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -171,7 +171,7 @@ namespace MWGui else valueStr = MyGUI::utility::toString(int(value)); - value = std::max(min, std::min(value, max)); + value = std::clamp(value, min, max); value = (value-min)/(max-min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); @@ -306,7 +306,7 @@ namespace MWGui mWaterTextureSize->setIndexSelected(2); int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); + waterReflectionDetail = std::clamp(waterReflectionDetail, 0, 5); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); updateMaxLightsComboBox(mMaxLights); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 8e6f951291..0830af0744 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -599,8 +599,7 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - int rank = factionPair.second; - rank = std::max(0, std::min(9, rank)); + const int rank = std::clamp(factionPair.second, 0, 9); text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index fa10ce03cd..bdb46e31a8 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -70,7 +70,7 @@ namespace MWInput } float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); - deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); + deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); } diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 7810a40ad2..f2bd4505d1 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -245,8 +245,8 @@ namespace MWInput mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); - mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); + mGuiCursorX = std::clamp(mGuiCursorX, 0.f, viewSize.width - 1); + mGuiCursorY = std::clamp(mGuiCursorY, 0.f, viewSize.height - 1); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index cb4f8237e7..32562591a5 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1013,13 +1013,10 @@ namespace MWMechanics void Actors::updateProcessingRange() { // We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876) - static const float maxProcessingRange = 7168.f; - static const float minProcessingRange = maxProcessingRange / 2.f; + static const float maxRange = 7168.f; + static const float minRange = maxRange / 2.f; - float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); - actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); - actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); - mActorsProcessingRange = actorsProcessingRange; + mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) @@ -1315,7 +1312,7 @@ namespace MWMechanics angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); osg::Vec2f posAtT = relPos + relSpeed * t; float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); - coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); + coef *= std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); movementCorrection = posAtT * coef; if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) // In case of dead body still try to go around (it looks natural), but reduce the correction twice. diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 58339d0228..36d4446580 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2044,7 +2044,7 @@ void CharacterController::update(float duration) mIsMovingBackward = vec.y() < 0; float maxDelta = osg::PI * duration * (2.5f - cosDelta); - delta = osg::clampBetween(delta, -maxDelta, maxDelta); + delta = std::clamp(delta, -maxDelta, maxDelta); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } @@ -2286,7 +2286,7 @@ void CharacterController::update(float duration) float swimmingPitch = mAnimation->getBodyPitchRadians(); float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; float maxSwimPitchDelta = 3.0f * duration; - swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); + swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } else @@ -2522,7 +2522,7 @@ void CharacterController::unpersistAnimationState() { float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); + float time = std::clamp(anim.mTime, start, stop); complete = (time - start) / (stop - start); } @@ -2746,7 +2746,7 @@ void CharacterController::setVisibility(float visibility) float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { - alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f)); + alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f); } visibility = std::min(visibility, alpha); @@ -2965,8 +2965,8 @@ void CharacterController::updateHeadTracking(float duration) const double xLimit = osg::DegreesToRadians(40.0); const double zLimit = osg::DegreesToRadians(30.0); double zLimitOffset = mAnimation->getUpperBodyYawRadians(); - xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit); - zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); + xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit); + zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); float factor = duration*5; factor = std::min(factor, 1.f); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index c1d5f711fc..2962a25ede 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -113,10 +113,9 @@ namespace MWMechanics + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); - int x = int(blockerTerm - attackerTerm); - int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); - int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); - x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); + const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); + int x = std::clamp(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance); if (Misc::Rng::roll0to99() < x) { diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp index 2376989745..e973e0ed52 100644 --- a/apps/openmw/mwmechanics/difficultyscaling.cpp +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr const MWWorld::Ptr& player = MWMechanics::getPlayer(); // [-500, 500] - int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); - difficultySetting = std::min(difficultySetting, 500); - difficultySetting = std::max(difficultySetting, -500); + const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500); static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 078cbc5f43..a1870cbdb0 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -356,10 +356,10 @@ namespace MWMechanics ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { - static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game"))); + static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f); MWWorld::Ptr player = getPlayer(); - int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); - count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); + count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); + count = std::clamp(getGemCharge() * multiplier / enchantPoints, 1, count); } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index e01ed3c425..362df54ab7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -545,9 +545,9 @@ namespace MWMechanics x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - if(clamp) - return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return int(x); + if (clamp) + return std::clamp(x, 0, 100);//, normally clamped to [0..100] when used + return static_cast(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) @@ -649,9 +649,9 @@ namespace MWMechanics int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s))))); + std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100)); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); + std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100)); } float c = -std::abs(floor(r * fPerDieRollMult)); @@ -689,10 +689,10 @@ namespace MWMechanics float s = c * fPerDieRollMult * fPerTempMult; int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); - npcStats.setAiSetting (CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); + npcStats.setAiSetting(CreatureStats::AI_Flee, + std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100)); + npcStats.setAiSetting(CreatureStats::AI_Fight, + std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100)); } x = floor(-c * fPerDieRollMult); diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 5d19368bf6..1d1dfacce8 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine - mReputation = std::min(255, std::max(0, reputation)); + mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 7907642f5e..566fb9eded 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -631,7 +631,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co if (!target.isInCell() || !target.getCell()->isExterior() || godmode) break; float time = world->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float timeDiff = std::clamp(std::abs(time - 13.f), 0.f, 7.f); float damageScale = 1.f - timeDiff / 7.f; // When cloudy, the sun damage effect is halved static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index b18ad288ad..70167abcd3 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -162,7 +162,10 @@ namespace MWMechanics float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + if (cap) + return std::clamp(castChance, 0.f, 100.f); + + return std::max(castChance, 0.f); } float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 570e89a17d..1a17cc87e6 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -128,8 +128,7 @@ namespace MWMechanics } // Take hit chance in account, but do not allow rating become negative. - float chance = getHitChance(actor, enemy, value) / 100.f; - rating *= std::min(1.f, std::max(0.01f, chance)); + rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index fc8725f5f4..a01ab96301 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -15,9 +15,9 @@ namespace MWPhysics const btVector3& position, const btScalar radius) { const btVector3 nearest( - std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), - std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), - std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) + std::clamp(position.x(), aabbMin.x(), aabbMax.x()), + std::clamp(position.y(), aabbMin.y(), aabbMax.y()), + std::clamp(position.z(), aabbMin.z(), aabbMax.z()) ); return nearest.distance(position) < radius; } diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index ea595e6abb..98e3bcf737 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -718,7 +718,7 @@ namespace MWPhysics physicActor->setCanWaterWalk(waterCollision); // Slow fall reduces fall speed by a factor of (effect magnitude / 200) - const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); + const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index e750fcad46..d1e842790b 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -236,7 +236,7 @@ namespace MWRender mTotalMovement += speed * duration; speed /= (1.f + speed / 500.f); float maxDelta = 300.f * duration; - mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); + mSmoothedSpeed += std::clamp(speed - mSmoothedSpeed, -maxDelta, maxDelta); mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); updateStandingPreviewMode(); @@ -434,7 +434,7 @@ namespace MWRender { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; - mPitch = osg::clampBetween(angle, -limit, limit); + mPitch = std::clamp(angle, -limit, limit); } float Camera::getCameraDistance() const @@ -460,7 +460,7 @@ namespace MWRender } mIsNearest = mBaseCameraDistance <= mNearest; - mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); + mBaseCameraDistance = std::clamp(mBaseCameraDistance, mNearest, mFurthest); Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); } diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 70f0ff02bb..0eb0e7738a 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -562,8 +562,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) if (!segment.mFogOfWarImage) return false; - nX = std::max(0.f, std::min(1.f, nX)); - nY = std::max(0.f, std::min(1.f, nY)); + nX = std::clamp(nX, 0.f, 1.f); + nY = std::clamp(nY, 0.f, 1.f); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); @@ -648,7 +648,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient uint32_t clr = *(uint32_t*)data; uint8_t alpha = (clr >> 24); - alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); + alpha = std::min( alpha, (uint8_t) (std::clamp((sqrDist/sqrExploreRadius)*255, 0.f, 1.f))); uint32_t val = (uint32_t) (alpha << 24); if ( *data != val) { diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index b6f12dea37..4e21d33475 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -733,12 +733,8 @@ namespace MWRender } void clampToCell(osg::Vec3f& cellPos) { - osg::Vec2i min (mCell.x(), mCell.y()); - osg::Vec2i max (mCell.x()+1, mCell.y()+1); - if (cellPos.x() < min.x()) cellPos.x() = min.x(); - if (cellPos.x() > max.x()) cellPos.x() = max.x(); - if (cellPos.y() < min.y()) cellPos.y() = min.y(); - if (cellPos.y() > max.y()) cellPos.y() = max.y(); + cellPos.x() = std::clamp(cellPos.x(), mCell.x(), mCell.x() + 1); + cellPos.y() = std::clamp(cellPos.y(), mCell.y(), mCell.y() + 1); } osg::Vec3f mPosition; osg::Vec2i mCell; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index c4ef1d9d9b..4e2da15107 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -297,7 +297,8 @@ namespace MWRender , mViewDistance(Settings::Manager::getFloat("viewing distance", "Camera")) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) - , mFieldOfView(std::min(std::max(1.f, Settings::Manager::getFloat("field of view", "Camera")), 179.f)) + , mFieldOfView(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f)) + , mFirstPersonFieldOfView(std::clamp(Settings::Manager::getFloat("first person field of view", "Camera"), 1.f, 179.f)) { bool reverseZ = SceneUtil::getReverseZ(); @@ -517,8 +518,6 @@ namespace MWRender NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); - mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index d8f92d1d1f..895486ea31 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -305,8 +305,7 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f, std::max(0.0f, - Settings::Manager::getFloat("refraction scale", "Water"))); + const float refractionScale = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f); mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); @@ -400,7 +399,7 @@ private: unsigned int calcNodeMask() { int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(5, std::max(mInterior ? 2 : 0, reflectionDetail)); + reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); unsigned int extraMask = 0; if(reflectionDetail >= 1) extraMask |= Mask_Terrain; if(reflectionDetail >= 2) extraMask |= Mask_Static; @@ -583,7 +582,7 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) // Add animated textures std::vector > textures; - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; i &textures) { - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; i 0) { - chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 127)); runtime.pop(); arg0--; } diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index ae31d60949..a36615ee4a 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -40,7 +40,7 @@ void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) else if (mSampleType == SampleType_Float32) { value = *reinterpret_cast(&mQueue[sample*advance]); - value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already. } sum += value*value; @@ -64,8 +64,7 @@ float Sound_Loudness::getLoudnessAtTime(float sec) const if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; - size_t index = static_cast(sec * mSamplesPerSec); - index = std::max(0, std::min(index, mSamples.size()-1)); + size_t index = std::clamp(sec * mSamplesPerSec, 0, mSamples.size() - 1); return mSamples[index]; } diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp index cc4eac3d6d..fd79b97e9b 100644 --- a/apps/openmw/mwsound/volumesettings.cpp +++ b/apps/openmw/mwsound/volumesettings.cpp @@ -10,7 +10,7 @@ namespace MWSound { float clamp(float value) { - return std::max(0.0f, std::min(1.0f, value)); + return std::clamp(value, 0.f, 1.f); } } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d35f72e80c..cc982e4621 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1334,7 +1334,7 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - objRot[0] = osg::clampBetween(objRot[0], -osg::PI_2, osg::PI_2); + objRot[0] = std::clamp(objRot[0], -osg::PI_2, osg::PI_2); objRot[1] = Misc::normalizeAngle(objRot[1]); objRot[2] = Misc::normalizeAngle(objRot[2]); } @@ -1901,7 +1901,7 @@ namespace MWWorld const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); - MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); + MWBase::Environment::get().getWindowManager()->setBlindness(std::clamp(blind, 0, 100)); int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index 3c4a1e0b07..e97ad6b759 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -169,7 +169,7 @@ namespace ESM { float height = mLandData->mHeights[int(row * vertMult) * ESM::Land::LAND_SIZE + int(col * vertMult)]; height /= height > 0 ? 128.f : 16.f; - height = std::min(max, std::max(min, height)); + height = std::clamp(height, min, max); wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast(height); } } diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index da43cc38ec..76f554bec7 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -145,7 +145,7 @@ namespace Gui FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor) : mVFS(vfs) , mUserDataPath(userDataPath) - , mFontHeight(16) + , mFontHeight(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)) , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) @@ -153,9 +153,6 @@ namespace Gui else mEncoding = encoding; - int fontSize = Settings::Manager::getInt("font size", "GUI"); - mFontHeight = std::min(std::max(12, fontSize), 20); - MyGUI::ResourceManager::getInstance().unregisterLoadXmlDelegate("Resource"); MyGUI::ResourceManager::getInstance().registerLoadXmlDelegate("Resource") = MyGUI::newDelegate(this, &FontLoader::loadFontFromXml); } @@ -549,7 +546,7 @@ namespace Gui // to allow to configure font size via config file, without need to edit XML files. // Also we should take UI scaling factor in account. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)) * mScalingFactor; + resolution = std::clamp(resolution, 48, 960) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); @@ -591,7 +588,7 @@ namespace Gui // setup separate fonts with different Resolution to fit these windows. // These fonts have an internal prefix. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); + resolution = std::clamp(resolution, 48, 960); float currentX = Settings::Manager::getInt("resolution x", "Video"); float currentY = Settings::Manager::getInt("resolution y", "Video"); diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index ddded82156..956fe2e489 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -57,7 +57,7 @@ float ControllerFunction::calculate(float value) const } case Constant: default: - return std::min(mStopTime, std::max(mStartTime, time)); + return std::clamp(time, mStartTime, mStopTime); } } diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 8c3758e980..dfe4cf1507 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -27,8 +27,7 @@ namespace SceneUtil mShadowSettings->setLightNum(0); mShadowSettings->setReceivesShadowTraversalMask(~0u); - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight); mShadowSettings->setBaseShadowTextureUnit(8 - numberOfShadowMapsPerLight); @@ -36,7 +35,7 @@ namespace SceneUtil const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows"); if (maximumShadowMapDistance > 0) { - const float shadowFadeStart = std::min(std::max(0.f, Settings::Manager::getFloat("shadow fade start", "Shadows")), 1.f); + const float shadowFadeStart = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f); mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance); mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart); } @@ -78,8 +77,7 @@ namespace SceneUtil if (!Settings::Manager::getBool("enable shadows", "Shadows")) return; - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight; diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp index daa69f9202..16ebba3587 100644 --- a/components/widgets/fontwrapper.hpp +++ b/components/widgets/fontwrapper.hpp @@ -31,15 +31,11 @@ namespace Gui } private: - static int clamp(const int& value, const int& lowBound, const int& highBound) - { - return std::min(std::max(lowBound, value), highBound); - } std::string getFontSize() { // Note: we can not use the FontLoader here, so there is a code duplication a bit. - static const std::string fontSize = std::to_string(clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); + static const std::string fontSize = std::to_string(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); return fontSize; } }; diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp index e8ba226f70..c6ff9628ee 100644 --- a/components/widgets/numericeditbox.cpp +++ b/components/widgets/numericeditbox.cpp @@ -31,7 +31,7 @@ namespace Gui try { mValue = std::stoi(newCaption); - int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + int capped = std::clamp(mValue, mMinValue, mMaxValue); if (capped != mValue) { mValue = capped;