diff --git a/AUTHORS.md b/AUTHORS.md index a722ff7237..3422f28a92 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -173,6 +173,7 @@ Programmers vocollapse Yohaulticetl zelurker + James Carty (MrTopCat) Documentation ------------- @@ -181,9 +182,11 @@ Documentation Alejandro Sanchez (HiPhish) Bodillium Bret Curtis (psi29a) + David Walley (Loriel) Cramal Ryan Tucker (Ravenwing) sir_herrbatka + Diego Crespo Packagers --------- diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9e9f97a8..d6d8df78eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,14 @@ Bug #2872: Tab completion in console doesn't work with explicit reference Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3049: Drain and Fortify effects are not properly applied on health, magicka and fatigue + Bug #3072: Fatal error on AddItem that has a script containing Equip Bug #3249: Fixed revert function not updating views properly Bug #3374: Touch spells not hitting kwama foragers Bug #3486: [Mod] NPC Commands does not work Bug #3533: GetSpellEffects should detect effects with zero duration Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning + Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla Bug #3876: Landscape texture painting is misaligned Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters @@ -42,6 +44,7 @@ Bug #4286: Scripted animations can be interrupted Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4304: "Follow" not working as a second AI package Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4311: OpenMW does not handle RootCollisionNode correctly Bug #4327: Missing animations during spell/weapon stance switching @@ -95,6 +98,7 @@ Bug #4575: Weird result of attack animation blending with movement animations Bug #4576: Reset of idle animations when attack can not be started Bug #4591: Attack strength should be 0 if player did not hold the attack button + Feature #1645: Casting effects from objects Feature #2606: Editor: Implemented (optional) case sensitive global search Feature #3083: Play animation when NPC is casting spell via script Feature #3103: Provide option for disposition to get increased by successful trade diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index a886166253..3c46298b0b 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -536,7 +536,7 @@ namespace MWBase /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos) = 0; + virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0; virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, @@ -566,6 +566,9 @@ namespace MWBase virtual bool isPlayerInJail() const = 0; + virtual void setPlayerTraveling(bool traveling) = 0; + virtual bool isPlayerTraveling() const = 0; + virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; /// Return terrain height at \a worldPos position. diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index ce6d0d522c..0b35bd9de4 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -517,7 +517,7 @@ namespace MWGui // Give the script a chance to run once before we do anything else // this is important when setting pcskipequip as a reaction to onpcequip being set (bk_treasuryreport does this) - if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) + if (!force && !script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) { MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 7a4a9293c7..cf4fb1b5e0 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -154,6 +154,10 @@ namespace MWGui if (playerGoldsetPlayerTraveling(true); + if (!mPtr.getCell()->isExterior()) // Interior cell -> mages guild transport MWBase::Environment::get().getWindowManager()->playSound("mysticism cast"); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index e794e0aaa4..6f5b4eeb8d 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2,7 +2,6 @@ #include #include - #include #include #include @@ -404,7 +403,7 @@ namespace MWMechanics std::set playerAllies; getActorsSidingWith(MWMechanics::getPlayer(), playerAllies, cachedAllies); - bool isPlayerFollowerOrEscorter = std::find(playerAllies.begin(), playerAllies.end(), actor1) != playerAllies.end(); + bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); // If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them // Doesn't apply for player followers/escorters @@ -458,7 +457,7 @@ namespace MWMechanics // Do aggression check if actor2 is the player or a player follower or escorter if (!aggressive) { - if (againstPlayer || std::find(playerAllies.begin(), playerAllies.end(), actor2) != playerAllies.end()) + if (againstPlayer || playerAllies.find(actor2) != playerAllies.end()) { // Player followers and escorters with high fight should not initiate combat with the player or with // other player followers or escorters @@ -1777,38 +1776,35 @@ namespace MWMechanics std::list Actors::getActorsSidingWith(const MWWorld::Ptr& actor) { std::list list; - for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + for(PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { - const MWWorld::Class &cls = iter->first.getClass(); - const CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Ptr &iteratedActor = iter->first; + if (iteratedActor == getPlayer()) + continue; + + const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; - // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat packages before the Follow/Escort package - for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) - { - if ((*it)->sideWithTarget() && (*it)->getTarget() == actor) - { - list.push_back(iter->first); - break; - } - else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) - break; - } + // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package // Actors that are targeted by this actor's Follow or Escort packages also side with them - if (actor != getPlayer()) + for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package) { - const CreatureStats &stats2 = actor.getClass().getCreatureStats(actor); - for (std::list::const_iterator it2 = stats2.getAiSequence().begin(); it2 != stats2.getAiSequence().end(); ++it2) + const MWWorld::Ptr &target = (*package)->getTarget(); + if ((*package)->sideWithTarget() && !target.isEmpty()) { - if ((*it2)->sideWithTarget() && !(*it2)->getTarget().isEmpty()) + if (iteratedActor == actor) { - list.push_back((*it2)->getTarget()); - break; + list.push_back(target); } - else if ((*it2)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) - break; + else if (target == actor) + { + list.push_back(iteratedActor); + } + break; } + else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander) + break; } } return list; @@ -1819,17 +1815,21 @@ namespace MWMechanics std::list list; for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Ptr &iteratedActor = iter->first; + if (iteratedActor == getPlayer()) + continue; + + const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; - // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package - for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + // An actor counts as following if AiFollow is the current AiPackage, + // or there are only Combat and Wander packages before the AiFollow package + for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package) { - if ((*it)->followTargetThroughDoors() && (*it)->getTarget() == actor) - list.push_back(iter->first); - else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + if ((*package)->followTargetThroughDoors() && (*package)->getTarget() == actor) + list.push_back(iteratedActor); + else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander) break; } } @@ -1878,24 +1878,24 @@ namespace MWMechanics std::list list; for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Ptr &iteratedActor = iter->first; + if (iteratedActor == getPlayer()) + continue; + + const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; - // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package - for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + // An actor counts as following if AiFollow is the current AiPackage, + // or there are only Combat and Wander packages before the AiFollow package + for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package) { - if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) + if ((*package)->followTargetThroughDoors() && (*package)->getTarget() == actor) { - MWWorld::Ptr followTarget = (*it)->getTarget(); - if (followTarget.isEmpty()) - continue; - if (followTarget == actor) - list.push_back(static_cast(*it)->getFollowIndex()); + list.push_back(static_cast(*package)->getFollowIndex()); break; } - else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander) break; } } @@ -1907,14 +1907,14 @@ namespace MWMechanics std::vector neighbors; osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, aiProcessingDistance, neighbors); - for(std::vector::const_iterator iter(neighbors.begin());iter != neighbors.end();++iter) + for(auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor) { - const MWWorld::Class &cls = iter->getClass(); - const CreatureStats &stats = cls.getCreatureStats(*iter); - if (stats.isDead() || *iter == actor) + const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor); + if (stats.isDead() || *neighbor == actor) continue; + if (stats.getAiSequence().isInCombat(actor)) - list.push_front(*iter); + list.push_front(*neighbor); } return list; } @@ -1926,15 +1926,18 @@ namespace MWMechanics osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, aiProcessingDistance, neighbors); - std::list followers = getActorsFollowing(actor); - for(std::vector::const_iterator iter(neighbors.begin());iter != neighbors.end();++iter) + std::set followers; + getActorsFollowing(actor, followers); + for (auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor) { - const CreatureStats &stats = iter->getClass().getCreatureStats(*iter); - if (stats.isDead() || *iter == actor || iter->getClass().isPureWaterCreature(*iter)) + const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor); + if (stats.isDead() || *neighbor == actor || neighbor->getClass().isPureWaterCreature(*neighbor)) continue; - const bool isFollower = std::find(followers.begin(), followers.end(), *iter) != followers.end(); - if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*iter, actor) && !isFollower)) - list.push_back(*iter); + + const bool isFollower = followers.find(*neighbor) != followers.end(); + + if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*neighbor, actor) && !isFollower)) + list.push_back(*neighbor); } return list; } diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 48cb17f6d7..948ffb3aac 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -42,7 +42,19 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac return false; } - osg::Vec3f dir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); + osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); + if (target.getClass().isActor()) + { + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); + targetPos.z() += halfExtents.z() * 2 * 0.75f; + } + + osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); + actorPos.z() += halfExtents.z() * 2 * 0.75f; + + osg::Vec3f dir = targetPos - actorPos; + bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 753dc240ed..b64b3568f8 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -19,6 +19,7 @@ #include "aicombataction.hpp" #include "aipursue.hpp" #include "actorutil.hpp" +#include "../mwworld/class.hpp" namespace MWMechanics { @@ -122,6 +123,20 @@ bool AiSequence::isInCombat() const return false; } +bool AiSequence::isEngagedWithActor() const +{ + for (std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + { + MWWorld::Ptr target2 = (*it)->getTarget(); + if (!target2.isEmpty() && target2.getClass().isNpc()) + return true; + } + } + return false; +} + bool AiSequence::hasPackage(int typeId) const { for (std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 5c72bcc4ca..4d0482a985 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -88,6 +88,9 @@ namespace MWMechanics /// Is there any combat package? bool isInCombat () const; + /// Are we in combat with any other actor, who's also engaging us? + bool isEngagedWithActor () const; + /// Does this AI sequence have the given package type? bool hasPackage(int typeId) const; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index dbc2cd93cd..e7343e23a7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1,6 +1,7 @@ #include "mechanicsmanagerimp.hpp" #include +#include #include @@ -1445,11 +1446,12 @@ namespace MWMechanics if (target == getPlayer() || !attacker.getClass().isActor()) return false; - std::list followersAttacker = getActorsSidingWith(attacker); + std::set followersAttacker; + getActorsSidingWith(attacker, followersAttacker); MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); - if (std::find(followersAttacker.begin(), followersAttacker.end(), target) != followersAttacker.end()) + if (followersAttacker.find(target) != followersAttacker.end()) { statsTarget.friendlyHit(); @@ -1460,24 +1462,11 @@ namespace MWMechanics } } - // Attacking an NPC that is already in combat with any other NPC is not a crime - AiSequence& seq = statsTarget.getAiSequence(); - bool isFightingNpc = false; - for (std::list::const_iterator it = seq.begin(); it != seq.end(); ++it) - { - if ((*it)->getTypeId() == AiPackage::TypeIdCombat) - { - MWWorld::Ptr target2 = (*it)->getTarget(); - if (!target2.isEmpty() && target2.getClass().isNpc()) - isFightingNpc = true; - } - } - - if (target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) - && !isAggressive(target, attacker) && !isFightingNpc - && !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackage::TypeIdPursue)) + if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); + AiSequence& seq = statsTarget.getAiSequence(); + if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target) || attacker == getPlayer()) && !seq.isInCombat(attacker)) @@ -1504,6 +1493,14 @@ namespace MWMechanics return true; } + bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker) + { + MWMechanics::AiSequence seq = target.getClass().getCreatureStats(target).getAiSequence(); + return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) + && !isAggressive(target, attacker) && !seq.isEngagedWithActor() + && !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackage::TypeIdPursue); + } + void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) { if (attacker.isEmpty() || victim.isEmpty()) @@ -1516,11 +1513,10 @@ namespace MWMechanics return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); - if (victimStats.getCrimeId() == -1) - return; + const MWWorld::Ptr &player = getPlayer(); + bool canCommit = attacker == player && canCommitCrimeAgainst(victim, attacker); // For now we report only about crimes of player and player's followers - const MWWorld::Ptr &player = getPlayer(); if (attacker != player) { std::set playerFollowers; @@ -1529,6 +1525,9 @@ namespace MWMechanics return; } + if (!canCommit && victimStats.getCrimeId() == -1) + return; + // Simple check for who attacked first: if the player attacked first, a crimeId should be set // Doesn't handle possible edge case where no one reported the assault, but in such a case, // for bystanders it is not possible to tell who attacked first, anyway. diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index e9ec14685d..af12d4d98f 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -132,6 +132,12 @@ namespace MWMechanics /// @note No-op for non-player attackers virtual void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); + /// Checks if commiting a crime is currently valid + /// @param victim The actor being attacked + /// @param attacker The actor commiting the crime + /// @return true if the victim is a valid target for crime + virtual bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); + /// Utility to check if taking this item is illegal and calling commitCrime if so /// @param container The container the item is in; may be empty for an item in the world virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index b337fa6b76..76140013d4 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -24,6 +24,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwrender/animation.hpp" +#include "../mwrender/vismask.hpp" #include "npcstats.hpp" #include "actorutil.hpp" @@ -327,16 +328,19 @@ namespace MWMechanics } void CastSpell::launchMagicBolt () - { - osg::Vec3f fallbackDirection (0,1,0); + { + osg::Vec3f fallbackDirection(0, 1, 0); + osg::Vec3f offset(0, 0, 0); + if (!mTarget.isEmpty() && mTarget.getClass().isActor()) + offset.z() = MWBase::Environment::get().getWorld()->getHalfExtents(mTarget).z(); // Fall back to a "caster to target" direction if we have no other means of determining it // (e.g. when cast by a non-actor) if (!mTarget.isEmpty()) fallbackDirection = - osg::Vec3f(mTarget.getRefData().getPosition().asVec3())- - osg::Vec3f(mCaster.getRefData().getPosition().asVec3()); - + (mTarget.getRefData().getPosition().asVec3() + offset) - + (mCaster.getRefData().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); } @@ -999,11 +1003,13 @@ namespace MWMechanics return true; } - void CastSpell::playSpellCastingEffects(const std::string &spellid){ - + void CastSpell::playSpellCastingEffects(const std::string &spellid) + { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Spell *spell = store.get().find(spellid); + std::vector addedEffects; + for (std::vector::const_iterator iter = spell->mEffects.mList.begin(); iter != spell->mEffects.mList.end(); ++iter) { @@ -1012,18 +1018,56 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - if (animation && mCaster.getClass().isActor()) // TODO: Non-actors should also create a spell cast vfx even if they are disabled (animation == NULL) + const ESM::Static* castStatic; + + if (!effect->mCasting.empty()) + castStatic = store.get().find (effect->mCasting); + else + castStatic = store.get().find ("VFX_DefaultCast"); + + // check if the effect was already added + if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end()) + continue; + + std::string texture = effect->mParticle; + + float scale = 1.0f; + osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3()); + + if (animation && mCaster.getClass().isNpc()) { - const ESM::Static* castStatic; + // For NPC we should take race height as scaling factor + const ESM::NPC *npc = mCaster.get()->mBase; + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); - if (!effect->mCasting.empty()) - castStatic = store.get().find (effect->mCasting); - else - castStatic = store.get().find ("VFX_DefaultCast"); + const ESM::Race *race = + esmStore.get().find(npc->mRace); - std::string texture = effect->mParticle; + scale = npc->isMale() ? race->mData.mHeight.mMale : race->mData.mHeight.mFemale; + } + else + { + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(mCaster); - animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture); + // TODO: take a size of particle or NPC with height and weight = 1.0 as scale = 1.0 + float scaleX = halfExtents.x() * 2 / 60.f; + float scaleY = halfExtents.y() * 2 / 60.f; + float scaleZ = halfExtents.z() * 2 / 120.f; + + scale = std::max({ scaleX, scaleY, scaleZ }); + } + + // If the caster has no animation, add the effect directly to the effectManager + if (animation) + { + animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture, scale); + } + else + { + // We should set scale for effect manager manually + float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f; + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale); } if (animation && !mCaster.getClass().isActor()) @@ -1033,6 +1077,8 @@ namespace MWMechanics "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; + addedEffects.push_back("meshes\\" + castStatic->mModel); + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!effect->mCastSound.empty()) sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 8e1105b9f1..4da3ec4a8e 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -204,6 +205,110 @@ namespace std::vector > mToRemove; }; + class RemoveFinishedCallbackVisitor : public RemoveVisitor + { + public: + RemoveFinishedCallbackVisitor() + : RemoveVisitor() + , mEffectId(-1) + { + } + + RemoveFinishedCallbackVisitor(int effectId) + : RemoveVisitor() + , mEffectId(effectId) + { + } + + virtual void apply(osg::Node &node) + { + traverse(node); + } + + virtual void apply(osg::Group &group) + { + traverse(group); + + osg::Callback* callback = group.getUpdateCallback(); + if (callback) + { + // We should remove empty transformation nodes and finished callbacks here + MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); + bool finished = vfxCallback && vfxCallback->mFinished; + bool toRemove = vfxCallback && mEffectId >= 0 && vfxCallback->mParams.mEffectId == mEffectId; + if (finished || toRemove) + { + mToRemove.push_back(std::make_pair(group.asNode(), group.getParent(0))); + } + } + } + + virtual void apply(osg::MatrixTransform &node) + { + traverse(node); + } + + virtual void apply(osg::Geometry&) + { + } + + private: + int mEffectId; + }; + + class FindVfxCallbacksVisitor : public osg::NodeVisitor + { + public: + + std::vector mCallbacks; + + FindVfxCallbacksVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mEffectId(-1) + { + } + + FindVfxCallbacksVisitor(int effectId) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mEffectId(effectId) + { + } + + virtual void apply(osg::Node &node) + { + traverse(node); + } + + virtual void apply(osg::Group &group) + { + osg::Callback* callback = group.getUpdateCallback(); + if (callback) + { + MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); + if (vfxCallback) + { + if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) + { + mCallbacks.push_back(vfxCallback); + } + } + } + traverse(group); + } + + virtual void apply(osg::MatrixTransform &node) + { + traverse(node); + } + + virtual void apply(osg::Geometry&) + { + } + + private: + int mEffectId; + }; + // Removes all drawables from a graph. class CleanObjectRootVisitor : public RemoveVisitor { @@ -287,7 +392,6 @@ namespace } } }; - } namespace MWRender @@ -432,6 +536,42 @@ namespace MWRender const std::multimap& getTextKeys() const; }; + void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + traverse(node, nv); + + if (mFinished) + return; + + double newTime = nv->getFrameStamp()->getSimulationTime(); + if (mStartingTime == 0) + { + mStartingTime = newTime; + return; + } + + double duration = newTime - mStartingTime; + mStartingTime = newTime; + + mParams.mAnimTime->addTime(duration); + if (mParams.mAnimTime->getTime() >= mParams.mMaxControllerLength) + { + if (mParams.mLoop) + { + // Start from the beginning again; carry over the remainder + // Not sure if this is actually needed, the controller function might already handle loops + float remainder = mParams.mAnimTime->getTime() - mParams.mMaxControllerLength; + mParams.mAnimTime->resetTime(remainder); + } + else + { + // Remove effect immediately + mParams.mObjects.reset(); + mFinished = true; + } + } + } + class ResetAccumRootCallback : public osg::NodeCallback { public: @@ -1436,15 +1576,22 @@ namespace MWRender useQuadratic, quadraticValue, quadraticRadiusMult, useLinear, linearRadiusMult, linearValue); } - void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) + void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture, float scale) { if (!mObjectRoot.get()) return; // Early out if we already have this effect - for (std::vector::iterator it = mEffects.begin(); it != mEffects.end(); ++it) - if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename) + FindVfxCallbacksVisitor visitor(effectId); + mInsert->accept(visitor); + + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) + { + UpdateVfxCallback* callback = *it; + + if (loop && !callback->mFinished && callback->mParams.mLoop && callback->mParams.mBoneName == bonename) return; + } EffectParams params; params.mModelName = model; @@ -1459,83 +1606,64 @@ namespace MWRender parentNode = found->second; } - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, parentNode); + osg::ref_ptr trans = new osg::PositionAttitudeTransform; + trans->setScale(osg::Vec3f(scale, scale, scale)); + parentNode->addChild(trans); + + osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, trans); node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - params.mObjects = PartHolderPtr(new PartHolder(node)); - SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; node->accept(disableFreezeOnCullVisitor); - - params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); - node->setNodeMask(Mask_Effect); + params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); params.mLoop = loop; params.mEffectId = effectId; params.mBoneName = bonename; - + params.mObjects = PartHolderPtr(new PartHolder(node)); params.mAnimTime = std::shared_ptr(new EffectAnimationTime); + trans->addUpdateCallback(new UpdateVfxCallback(params)); SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(params.mAnimTime)); node->accept(assignVisitor); overrideFirstRootTexture(texture, mResourceSystem, node); - - // TODO: in vanilla morrowind the effect is scaled based on the host object's bounding box. - - mEffects.push_back(params); } void Animation::removeEffect(int effectId) { - for (std::vector::iterator it = mEffects.begin(); it != mEffects.end(); ++it) - { - if (it->mEffectId == effectId) - { - mEffects.erase(it); - return; - } - } + RemoveFinishedCallbackVisitor visitor(effectId); + mInsert->accept(visitor); + visitor.remove(); } void Animation::getLoopingEffects(std::vector &out) const { - for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + FindVfxCallbacksVisitor visitor; + mInsert->accept(visitor); + + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { - if (it->mLoop) - out.push_back(it->mEffectId); + UpdateVfxCallback* callback = *it; + + if (callback->mParams.mLoop && !callback->mFinished) + out.push_back(callback->mParams.mEffectId); } } void Animation::updateEffects(float duration) { - for (std::vector::iterator it = mEffects.begin(); it != mEffects.end(); ) - { - it->mAnimTime->addTime(duration); - - if (it->mAnimTime->getTime() >= it->mMaxControllerLength) - { - if (it->mLoop) - { - // Start from the beginning again; carry over the remainder - // Not sure if this is actually needed, the controller function might already handle loops - float remainder = it->mAnimTime->getTime() - it->mMaxControllerLength; - it->mAnimTime->resetTime(remainder); - } - else - { - it = mEffects.erase(it); - continue; - } - } - ++it; - } + // TODO: objects without animation still will have + // transformation nodes with finished callbacks + RemoveFinishedCallbackVisitor visitor; + mInsert->accept(visitor); + visitor.remove(); } bool Animation::upperBodyReady() const @@ -1778,5 +1906,4 @@ namespace MWRender mNode->getParent(0)->removeChild(mNode); } } - } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 43a6268994..1da1fe4efb 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -71,6 +71,17 @@ private: }; typedef std::shared_ptr PartHolderPtr; +struct EffectParams +{ + std::string mModelName; // Just here so we don't add the same effect twice + PartHolderPtr mObjects; + std::shared_ptr mAnimTime; + float mMaxControllerLength; + int mEffectId; + bool mLoop; + std::string mBoneName; +}; + class Animation : public osg::Referenced { public: @@ -247,19 +258,6 @@ protected: osg::Vec3f mAccumulate; - struct EffectParams - { - std::string mModelName; // Just here so we don't add the same effect twice - PartHolderPtr mObjects; - std::shared_ptr mAnimTime; - float mMaxControllerLength; - int mEffectId; - bool mLoop; - std::string mBoneName; - }; - - std::vector mEffects; - TextKeyListener* mTextKeyListener; osg::ref_ptr mHeadController; @@ -369,7 +367,7 @@ public: * @param texture override the texture specified in the model's materials - if empty, do not override * @note Will not add an effect twice. */ - void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = ""); + void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = "", float scale = 1.0f); void removeEffect (int effectId); void getLoopingEffects (std::vector& out) const; @@ -489,5 +487,24 @@ public: ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight); }; +class UpdateVfxCallback : public osg::NodeCallback +{ +public: + UpdateVfxCallback(EffectParams& params) + : mFinished(false) + , mParams(params) + , mStartingTime(0) + { + } + + bool mFinished; + EffectParams mParams; + + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv); + +private: + double mStartingTime; +}; + } #endif diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index e17fe3191e..ce0a41c9a0 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -49,6 +49,7 @@ #include #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" #include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -1321,6 +1322,29 @@ namespace MWRender } } + osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const + { + osg::Vec3f halfExtents(0, 0, 0); + std::string modelName = object.getClass().getModel(object); + if (modelName.empty()) + return halfExtents; + + osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(modelName); + osg::ComputeBoundsVisitor computeBoundsVisitor; + computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem|MWRender::Mask_Effect)); + const_cast(node.get())->accept(computeBoundsVisitor); + osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); + + if (bounds.valid()) + { + halfExtents[0] = std::abs(bounds.xMax() - bounds.xMin()) / 2.f; + halfExtents[1] = std::abs(bounds.yMax() - bounds.yMin()) / 2.f; + halfExtents[2] = std::abs(bounds.zMax() - bounds.zMin()) / 2.f; + } + + return halfExtents; + } + void RenderingManager::resetFieldOfView() { if (mFieldOfViewOverridden == true) diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index f8e7ba8bcc..b4473c3b4f 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -203,6 +203,8 @@ namespace MWRender /// reset a previous overrideFieldOfView() call, i.e. revert to field of view specified in the settings file. void resetFieldOfView(); + osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& object) const; + void exportSceneGraph(const MWWorld::Ptr& ptr, const std::string& filename, const std::string& format); LandManager* getLandManager() const; diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index dd3e54a88a..cf12d12c3a 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -192,8 +192,9 @@ namespace MWScript if (it == invStore.end()) { it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr); - Log(Debug::Warning) << "Implicitly adding one " << item << " to container " - "to fulfil requirements of Equip instruction"; + Log(Debug::Warning) << "Implicitly adding one " << item << + " to the inventory store of " << ptr.getCellRef().getRefId() << + " to fulfill the requirements of Equip instruction"; } if (ptr == MWMechanics::getPlayer()) diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 30d0c6feee..3d1978d625 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1082,6 +1082,7 @@ namespace MWScript MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr (targetId, false); MWMechanics::CastSpell cast(ptr, target, false, true); + cast.playSpellCastingEffects(spell->mId); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); @@ -1155,8 +1156,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime &runtime) { - /// \todo implement traveling check - runtime.push (0); + runtime.push (MWBase::Environment::get().getWorld()->isPlayerTraveling()); } }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 3e4f3ccd0b..c2ebac8fc5 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -155,7 +155,8 @@ namespace MWWorld mGodMode(false), mScriptsEnabled(true), mContentFiles (contentFiles), mUserDataPath(userDataPath), mActivationDistanceOverride (activationDistanceOverride), mStartupScript(startupScript), mStartCell (startCell), mDistanceToFacedObject(-1), mTeleportEnabled(true), - mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), mSpellPreloadTimer(0.f) + mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), + mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f) { mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode)); mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath)); @@ -313,6 +314,8 @@ namespace MWWorld mGoToJail = false; mTeleportEnabled = true; mLevitationEnabled = true; + mPlayerTraveling = false; + mPlayerInJail = false; fillGlobalVariables(); } @@ -1644,6 +1647,15 @@ namespace MWWorld if (mGoToJail && !paused) goToJail(); + // Reset "traveling" flag - there was a frame to detect traveling. + mPlayerTraveling = false; + + // The same thing for "in jail" flag: reset it if: + // 1. Player was in jail + // 2. Jailing window was closed + if (mPlayerInJail && !mGoToJail && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail)) + mPlayerInJail = false; + updateWeather(duration, paused); if (!paused) @@ -3283,6 +3295,7 @@ namespace MWWorld { // Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first) mGoToJail = true; + mPlayerInJail = true; MWWorld::Ptr player = getPlayerPtr(); @@ -3308,10 +3321,17 @@ namespace MWWorld bool World::isPlayerInJail() const { - if (mGoToJail) - return true; + return mPlayerInJail; + } - return MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail); + void World::setPlayerTraveling(bool traveling) + { + mPlayerTraveling = traveling; + } + + bool World::isPlayerTraveling() const + { + return mPlayerTraveling; } float World::getTerrainHeightAt(const osg::Vec3f& worldPos) const @@ -3319,12 +3339,16 @@ namespace MWWorld return mRendering->getTerrainHeightAt(worldPos); } - osg::Vec3f World::getHalfExtents(const ConstPtr& actor, bool rendering) const + osg::Vec3f World::getHalfExtents(const ConstPtr& object, bool rendering) const { + if (!object.getClass().isActor()) + return mRendering->getHalfExtents(object); + + // Handle actors separately because of bodyparts if (rendering) - return mPhysics->getRenderingHalfExtents(actor); + return mPhysics->getRenderingHalfExtents(object); else - return mPhysics->getHalfExtents(actor); + return mPhysics->getHalfExtents(object); } std::string World::exportSceneGraph(const Ptr &ptr) @@ -3383,9 +3407,9 @@ namespace MWWorld mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); } - void World::spawnEffect(const std::string &model, const std::string &textureOverride, const osg::Vec3f &worldPos) + void World::spawnEffect(const std::string &model, const std::string &textureOverride, const osg::Vec3f &worldPos, float scale, bool isMagicVFX) { - mRendering->spawnEffect(model, textureOverride, worldPos); + mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX); } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index a2616995ad..432991059a 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -168,6 +168,8 @@ namespace MWWorld bool mLevitationEnabled; bool mGoToJail; int mDaysInPrison; + bool mPlayerTraveling; + bool mPlayerInJail; float mSpellPreloadTimer; @@ -644,7 +646,7 @@ namespace MWWorld /// Spawn a blood effect for \a ptr at \a worldPosition void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; - void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos) override; + void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) override; void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, @@ -672,6 +674,9 @@ namespace MWWorld bool isPlayerInJail() const override; + void setPlayerTraveling(bool traveling) override; + bool isPlayerTraveling() const override; + /// Return terrain height at \a worldPos position. float getTerrainHeightAt(const osg::Vec3f& worldPos) const override; diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 2d551348de..602bb826fb 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -557,7 +557,7 @@ namespace Compiler mExplicit.clear(); } - void GetArgumentsFromMessageFormat::visitedPlaceholder(Placeholder placeholder, char /*padding*/, int /*width*/, int /*precision*/) + void GetArgumentsFromMessageFormat::visitedPlaceholder(Placeholder placeholder, char /*padding*/, int /*width*/, int /*precision*/, Notation /*notation*/) { switch (placeholder) { diff --git a/components/compiler/lineparser.hpp b/components/compiler/lineparser.hpp index d92c4895e2..8f7f64bf2c 100644 --- a/components/compiler/lineparser.hpp +++ b/components/compiler/lineparser.hpp @@ -83,7 +83,7 @@ namespace Compiler std::string mArguments; protected: - virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision); + virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation); virtual void visitedCharacter(char c) {} public: diff --git a/components/interpreter/miscopcodes.hpp b/components/interpreter/miscopcodes.hpp index 20a1b52bec..03b7e186f5 100644 --- a/components/interpreter/miscopcodes.hpp +++ b/components/interpreter/miscopcodes.hpp @@ -24,7 +24,7 @@ namespace Interpreter Runtime& mRuntime; protected: - virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision) + virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation) { std::ostringstream out; out.fill(padding); @@ -58,8 +58,34 @@ namespace Interpreter float value = mRuntime[0].mFloat; mRuntime.pop(); - out << std::fixed << value; - mFormattedMessage += out.str(); + if (notation == FixedNotation) + { + out << std::fixed << value; + mFormattedMessage += out.str(); + } + else if (notation == ShortestNotation) + { + std::string scientific; + std::string fixed; + + out << std::scientific << value; + + scientific = out.str(); + + out.str(std::string()); + out.clear(); + + out << std::fixed << value; + + fixed = out.str(); + + mFormattedMessage += fixed.length() < scientific.length() ? fixed : scientific; + } + else + { + out << std::scientific << value; + mFormattedMessage += out.str(); + } } break; default: diff --git a/components/misc/messageformatparser.cpp b/components/misc/messageformatparser.cpp index 3a35c83eaf..6f0e471325 100644 --- a/components/misc/messageformatparser.cpp +++ b/components/misc/messageformatparser.cpp @@ -49,11 +49,15 @@ namespace Misc width = (widthSet) ? width : -1; if (m[i] == 'S' || m[i] == 's') - visitedPlaceholder(StringPlaceholder, pad, width, precision); - else if (m[i] == 'g' || m[i] == 'G') - visitedPlaceholder(IntegerPlaceholder, pad, width, precision); + visitedPlaceholder(StringPlaceholder, pad, width, precision, FixedNotation); + else if (m[i] == 'd' || m[i] == 'i') + visitedPlaceholder(IntegerPlaceholder, pad, width, precision, FixedNotation); else if (m[i] == 'f' || m[i] == 'F') - visitedPlaceholder(FloatPlaceholder, pad, width, precision); + visitedPlaceholder(FloatPlaceholder, pad, width, precision, FixedNotation); + else if (m[i] == 'e' || m[i] == 'E') + visitedPlaceholder(FloatPlaceholder, pad, width, precision, ScientificNotation); + else if (m[i] == 'g' || m[i] == 'G') + visitedPlaceholder(FloatPlaceholder, pad, width, precision, ShortestNotation); } } } diff --git a/components/misc/messageformatparser.hpp b/components/misc/messageformatparser.hpp index c12b9352ae..db2a8b0af4 100644 --- a/components/misc/messageformatparser.hpp +++ b/components/misc/messageformatparser.hpp @@ -15,7 +15,14 @@ namespace Misc FloatPlaceholder }; - virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision) = 0; + enum Notation + { + FixedNotation, + ScientificNotation, + ShortestNotation + }; + + virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation) = 0; virtual void visitedCharacter(char c) = 0; public: diff --git a/docs/source/manuals/openmw-cs/index.rst b/docs/source/manuals/openmw-cs/index.rst index f124b526f2..ee82907119 100644 --- a/docs/source/manuals/openmw-cs/index.rst +++ b/docs/source/manuals/openmw-cs/index.rst @@ -9,7 +9,7 @@ few chapters to familiarise yourself with the new interface. .. warning:: OpenMW CS is still software in development. The manual does not cover any of - its shortcomings, it is written as if everything was working as inteded. + its shortcomings, it is written as if everything was working as intended. Please report any software problems as bugs in the software, not errors in the manual. diff --git a/docs/source/manuals/openmw-cs/record-filters.rst b/docs/source/manuals/openmw-cs/record-filters.rst index a579d8dd88..19eee2542c 100644 --- a/docs/source/manuals/openmw-cs/record-filters.rst +++ b/docs/source/manuals/openmw-cs/record-filters.rst @@ -121,7 +121,7 @@ existing filters into more complex ones. Scopes ====== -Every default filter has the prefix ``project``. This is a *scpoe*, a mechanism +Every default filter has the prefix ``project``. This is a *scope*, a mechanism that determines the lifetime of the filter. These are the supported scopes: ``project::`` diff --git a/docs/source/manuals/openmw-cs/tour.rst b/docs/source/manuals/openmw-cs/tour.rst index 5d7034deb3..fbd4fd0478 100644 --- a/docs/source/manuals/openmw-cs/tour.rst +++ b/docs/source/manuals/openmw-cs/tour.rst @@ -236,7 +236,7 @@ a negative number indicating that he will restock again to maintain that level. However, it's an attractive item, so he will probably wear it rather than sell it. So set his stock level too high for him to wear them all (3 works, 2 might do). -Another possibilty, again in Seyda Neen making it easy to access, would be for +Another possibility, again in Seyda Neen making it easy to access, would be for Fargoth to give it to the player in exchange for his healing ring. .. figure:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/manuals/openmw-cs/_static/images/chapter-1/Ring_to_Fargoth_1.png @@ -360,7 +360,7 @@ the base game. "Modified" status will cover items from the base game which have been modified in this addon. Click on the top of the column to toggle between ascending and descending order - thus between "Added" -and "Modified" at the top. Or put your desired modified status into a filter then sort alpabetically +and "Modified" at the top. Or put your desired modified status into a filter then sort alphabetically on a different column.