diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a5566be..b21f328ea 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 39842db66..774bf3aea 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 e7df0b7d3..324c5ea77 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 3a501a93f..55d2689f8 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 e38d98b5c..b13741187 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 bdc0dda88..0650c21a2 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 1e8eb9f8e..8537060f6 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 9b9092146..ab5dc8192 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 000000000..83280ec2f --- /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 000000000..b93ff7f10 --- /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 6725fb935..8c17dc0a9 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 4564d2fa3..b06e8b8ce 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 a63123bcf..5080c0923 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 11932e690..8af45e36a 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 ff47d3e56..a727b4b3a 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 ab1ba2f61..adf60b360 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 01aeb2fc1..af43eb414 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 a1aa74cab..d6a459b1b 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;