diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a5566be1..b21f328ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ Feature #4968: Scalable UI widget skins Feature #4994: Persistent pinnable windows hiding Feature #5000: Compressed BSA format support + Feature #5010: Native graphics herbalism support Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4721: Add NMake support to the Windows prebuild script diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 39842db665..774bf3aea8 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -62,7 +62,7 @@ add_openmw_dir (mwsound add_openmw_dir (mwworld refdata worldimp scene globals class action nullaction actionteleport containerstore actiontalk actiontake manualref player cellvisitors failedaction - cells localscripts customdata inventorystore ptr actionopen actionread + cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref physicssystem weather projectilemanager diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index e7df0b7d35..324c5ea77d 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -578,6 +578,7 @@ namespace MWBase /// Return the distance between actor's weapon and target's collision box. virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; + virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; virtual bool isPlayerInJail() const = 0; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 3a501a93f9..55d2689f84 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -15,6 +15,7 @@ #include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/actionharvest.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwphysics/physicssystem.hpp" @@ -22,6 +23,7 @@ #include "../mwgui/tooltips.hpp" +#include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -40,6 +42,10 @@ namespace MWClass { return *this; } + virtual const ContainerCustomData& asContainerCustomData() const + { + return *this; + } }; MWWorld::CustomData *ContainerCustomData::clone() const @@ -63,9 +69,20 @@ namespace MWClass // store ptr.getRefData().setCustomData (data.release()); + + MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } } + bool canBeHarvested(const MWWorld::ConstPtr& ptr) + { + const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation == nullptr) + return false; + + return animation->canBeHarvested(); + } + void Container::respawn(const MWWorld::Ptr &ptr) const { MWWorld::LiveCellRef *ref = @@ -175,6 +192,12 @@ namespace MWClass { if(!isTrapped) { + if (canBeHarvested(ptr)) + { + std::shared_ptr action (new MWWorld::ActionHarvest(ptr)); + return action; + } + std::shared_ptr action (new MWWorld::ActionOpen(ptr)); return action; } @@ -225,9 +248,18 @@ namespace MWClass bool Container::hasToolTip (const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + if (getName(ptr).empty()) + return false; - return (ref->mBase->mName != ""); + if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) + return !canBeHarvested(ptr) || data->asContainerCustomData().mContainerStore.hasVisibleItems(); + + return true; + } + + bool Container::canBeActivated(const MWWorld::Ptr& ptr) const + { + return hasToolTip(ptr); } MWGui::ToolTipInfo Container::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index e38d98b5c2..b137411870 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -63,6 +63,8 @@ namespace MWClass const; ///< Write additional state from \a ptr into \a state. + virtual bool canBeActivated(const MWWorld::Ptr& ptr) const; + static void registerSelf(); virtual void respawn (const MWWorld::Ptr& ptr) const; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index bdc0dda889..0650c21a23 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -949,6 +949,9 @@ namespace MWMechanics return true; } + if (!target.getClass().canBeActivated(target)) + return true; + // TODO: implement a better check to check if target is owned bed if (target.getClass().isActivator() && target.getClass().getScript(target).compare(0, 3, "Bed") != 0) return true; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 1e8eb9f8ef..8537060f6b 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -131,6 +131,25 @@ namespace } }; + class HarvestVisitor : public osg::NodeVisitor + { + public: + HarvestVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + { + } + + virtual void apply(osg::Switch& node) + { + if (node.getName() == Constants::HerbalismLabel) + { + node.setSingleChildOn(1); + } + + traverse(node); + } + }; + NifOsg::TextKeyMap::const_iterator findGroupStart(const NifOsg::TextKeyMap &keys, const std::string &groupname) { NifOsg::TextKeyMap::const_iterator iter(keys.begin()); @@ -1970,6 +1989,30 @@ namespace MWRender AddSwitchCallbacksVisitor visitor; mObjectRoot->accept(visitor); } + + if (ptr.getTypeName() == typeid(ESM::Container).name() && + SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel) && + ptr.getRefData().getCustomData() != nullptr) + { + const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + if (!store.hasVisibleItems()) + { + HarvestVisitor visitor; + mObjectRoot->accept(visitor); + } + } + } + + bool ObjectAnimation::canBeHarvested() const + { + if (mPtr.getTypeName() != typeid(ESM::Container).name()) + return false; + + const MWWorld::LiveCellRef* ref = mPtr.get(); + if (!(ref->mBase->mFlags & ESM::Container::Organic)) + return false; + + return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } Animation::AnimState::~AnimState() diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 9b90921463..ab5dc8192f 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -475,6 +475,7 @@ public: virtual float getHeadPitch() const; virtual float getHeadYaw() const; virtual void setAccurateAiming(bool enabled) {} + virtual bool canBeHarvested() const { return false; } private: Animation(const Animation&); @@ -484,6 +485,8 @@ private: class ObjectAnimation : public Animation { public: ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight); + + bool canBeHarvested() const; }; class UpdateVfxCallback : public osg::NodeCallback diff --git a/apps/openmw/mwworld/actionharvest.cpp b/apps/openmw/mwworld/actionharvest.cpp new file mode 100644 index 0000000000..83280ec2fa --- /dev/null +++ b/apps/openmw/mwworld/actionharvest.cpp @@ -0,0 +1,93 @@ +#include "actionharvest.hpp" + +#include + +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "class.hpp" +#include "containerstore.hpp" + +namespace MWWorld +{ + ActionHarvest::ActionHarvest (const MWWorld::Ptr& container) + : Action (true, container) + { + setSound("Item Ingredient Up"); + } + + void ActionHarvest::executeImp (const MWWorld::Ptr& actor) + { + if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) + return; + + MWWorld::Ptr target = getTarget(); + MWWorld::ContainerStore& store = target.getClass().getContainerStore (target); + MWWorld::ContainerStore& actorStore = actor.getClass().getContainerStore(actor); + std::map takenMap; + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + if (!it->getClass().showsInInventory(*it)) + continue; + + int itemCount = it->getRefData().getCount(); + // Note: it is important to check for crime before move an item from container. Otherwise owner check will not work + // for a last item in the container - empty harvested containers are considered as "allowed to use". + MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, *it, target, itemCount); + actorStore.add(*it, itemCount, actor); + store.remove(*it, itemCount, getTarget()); + takenMap[it->getClass().getName(*it)]+=itemCount; + } + + // Spawn a messagebox (only for items added to player's inventory) + if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + std::ostringstream stream; + int lineCount = 0; + const static int maxLines = 10; + for (auto & pair : takenMap) + { + std::string itemName = pair.first; + int itemCount = pair.second; + lineCount++; + if (lineCount == maxLines) + stream << "\n..."; + else if (lineCount > maxLines) + break; + + // The two GMST entries below expand to strings informing the player of what, and how many of it has been added to their inventory + std::string msgBox; + if (itemCount == 1) + { + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage60}"); + Misc::StringUtils::replace(msgBox, "%s", itemName.c_str(), 2); + } + else + { + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage61}"); + Misc::StringUtils::replace(msgBox, "%d", std::to_string(itemCount).c_str(), 2); + Misc::StringUtils::replace(msgBox, "%s", itemName.c_str(), 2); + } + + stream << msgBox; + } + std::string tooltip = stream.str(); + // remove the first newline (easier this way) + if (tooltip.size() > 0 && tooltip[0] == '\n') + tooltip.erase(0, 1); + + if (tooltip.size() > 0) + MWBase::Environment::get().getWindowManager()->messageBox(tooltip); + } + + // Update animation object + MWBase::Environment::get().getWorld()->disable(target); + MWBase::Environment::get().getWorld()->enable(target); + } +} diff --git a/apps/openmw/mwworld/actionharvest.hpp b/apps/openmw/mwworld/actionharvest.hpp new file mode 100644 index 0000000000..b93ff7f109 --- /dev/null +++ b/apps/openmw/mwworld/actionharvest.hpp @@ -0,0 +1,19 @@ +#ifndef GAME_MWWORLD_ACTIONHARVEST_H +#define GAME_MWWORLD_ACTIONHARVEST_H + +#include "action.hpp" +#include "ptr.hpp" + +namespace MWWorld +{ + class ActionHarvest : public Action + { + virtual void executeImp (const MWWorld::Ptr& actor); + + public: + ActionHarvest (const Ptr& container); + ///< \param container The Container the Player has activated. + }; +} + +#endif // ACTIONOPEN_H diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 6725fb9350..8c17dc0a9d 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -422,6 +422,17 @@ int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const return count - toRemove; } +bool MWWorld::ContainerStore::hasVisibleItems() const +{ + for (auto iter(begin()); iter != end(); ++iter) + { + if (iter->getClass().showsInInventory(*iter)) + return true; + } + + return false; +} + int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor) { assert(this == item.getContainerStore()); diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 4564d2fa3c..b06e8b8ce6 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -128,6 +128,8 @@ namespace MWWorld ContainerStoreIterator begin (int mask = Type_All); ContainerStoreIterator end(); + bool hasVisibleItems() const; + virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool setOwner=false); ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) /// diff --git a/apps/openmw/mwworld/customdata.cpp b/apps/openmw/mwworld/customdata.cpp index a63123bcf5..5080c09230 100644 --- a/apps/openmw/mwworld/customdata.cpp +++ b/apps/openmw/mwworld/customdata.cpp @@ -42,6 +42,13 @@ MWClass::ContainerCustomData &CustomData::asContainerCustomData() throw std::logic_error(error.str()); } +const MWClass::ContainerCustomData &CustomData::asContainerCustomData() const +{ + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; + throw std::logic_error(error.str()); +} + MWClass::DoorCustomData &CustomData::asDoorCustomData() { std::stringstream error; diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 11932e690f..8af45e36ad 100644 --- a/apps/openmw/mwworld/customdata.hpp +++ b/apps/openmw/mwworld/customdata.hpp @@ -30,6 +30,7 @@ namespace MWWorld virtual const MWClass::NpcCustomData& asNpcCustomData() const; virtual MWClass::ContainerCustomData& asContainerCustomData(); + virtual const MWClass::ContainerCustomData& asContainerCustomData() const; virtual MWClass::DoorCustomData& asDoorCustomData(); virtual const MWClass::DoorCustomData& asDoorCustomData() const; diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index ff47d3e564..a727b4b3a8 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -42,6 +42,11 @@ namespace bool operator()(const MWWorld::Ptr& containerPtr) { + // Ignore containers without generated content + if (containerPtr.getTypeName() == typeid(ESM::Container).name() && + containerPtr.getRefData().getCustomData() == nullptr) + return false; + MWWorld::ContainerStore& container = containerPtr.getClass().getContainerStore(containerPtr); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) { diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index ab1ba2f613..adf60b360c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -141,9 +141,9 @@ namespace MWWorld MWWorld::Ptr getFacedObject(float maxDistance, bool ignorePlayer=true); public: // FIXME + void addContainerScripts(const Ptr& reference, CellStore* cell) override; void removeContainerScripts(const Ptr& reference) override; private: - void addContainerScripts(const Ptr& reference, CellStore* cell); void PCDropped (const Ptr& item); void processDoors(float duration); diff --git a/components/misc/constants.hpp b/components/misc/constants.hpp index 01aeb2fc1e..af43eb414d 100644 --- a/components/misc/constants.hpp +++ b/components/misc/constants.hpp @@ -27,6 +27,9 @@ const int CellSizeInUnits = 8192; // A label to mark night/day visual switches const std::string NightDayLabel = "NightDaySwitch"; +// A label to mark visual switches for herbalism feature +const std::string HerbalismLabel = "HerbalismSwitch"; + } #endif diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index a1aa74cab9..d6a459b1b1 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -635,6 +635,8 @@ namespace NifOsg const Nif::NiSwitchNode* niSwitchNode = static_cast(nifNode); if (niSwitchNode->name == Constants::NightDayLabel && !SceneUtil::hasUserDescription(rootNode, Constants::NightDayLabel)) rootNode->getOrCreateUserDataContainer()->addDescription(Constants::NightDayLabel); + else if (niSwitchNode->name == Constants::HerbalismLabel && !SceneUtil::hasUserDescription(rootNode, Constants::HerbalismLabel)) + rootNode->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel); } return node;