diff --git a/AUTHORS.md b/AUTHORS.md index 82372ae36..17c20e73d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -101,6 +101,7 @@ Programmers Nikolay Kasyanov (corristo) nobrakal Nolan Poe (nopoe) + Oleg Chkan (mrcheko) Paul Cercueil (pcercuei) Paul McElroy (Greendogo) Pi03k diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d427fa82..0add2e02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +0.40.0 +------ + + Bug #1320: AiWander - Creatures in cells without pathgrids do not wander + Bug #1873: Death events are triggered at the beginning of the death animation + Bug #1996: Resting interrupts magic effects + Bug #2399: Vampires can rest in broad daylight and survive the experience + Bug #2604: Incorrect magicka recalculation + Bug #2721: Telekinesis extends interaction range where it shouldn't + Bug #2981: When waiting, NPCs can go where they wouldn't go normally. + Bug #3045: Esp files containing the letter '#' in the file name cannot be loaded on startup + Bug #3071: Slowfall does not stop momentum when jumping + Bug #3085: Plugins can not replace parent cell references with a cell reference of different type + Bug #3145: Bug with AI Cliff Racer. He will not attack you, unless you put in front of him. + Bug #3149: Editor: Weather tables were missing from regions + Bug #3201: Netch shoots over your head + Bug #3269: If you deselect a mod and try to load a save made inside a cell added by it, you end bellow the terrain in the grid 0/0 + Bug #3286: Editor: Script editor tab width + Bug #3329: Teleportation spells cause crash to desktop after build update from 0.37 to 0.38.0 + Bug #3331: Editor: Start Scripts table: Adding a script doesn't refresh the list of Start Scripts and allows to add a single script multiple times + Bug #3332: Editor: Scene view: Tool tips only occur when holding the left mouse button + Bug #3340: ESS-Importer does not separate item stacks + Bug #3342: Editor: Creation of pathgrids did not check if the pathgrid already existed + Bug #3346: "Talked to PC" is always 0 for "Hello" dialogue + Bug #3349: AITravel doesn't repeat + Bug #3370: NPCs wandering to invalid locations after training + Bug #3378: "StopCombat" command does not function in vanilla quest + Bug #3384: Battle at Nchurdamz - Larienna Macrina does not stop combat after killing Hrelvesuu + Bug #3388: Monster Respawn tied to Quicksave + Bug #3390: Strange visual effect in Dagoth Ur's chamber + Bug #3391: Inappropriate Blight weather behavior at end of main quest + Bug #3394: Replaced dialogue inherits some of its old data + Bug #3397: Actors that start the game dead always have the same death pose + Bug #3401: Sirollus Saccus sells not glass arrows + Bug #3402: Editor: Weapon data not being properly set + Bug #3405: Mulvisic Othril will not use her chitin throwing stars + Bug #3407: Tanisie Verethi will immediately detect the player + Bug #3408: Improper behavior of ashmire particles + Bug #3412: Ai Wander start time resets when saving/loading the game + Bug #3416: 1st person and 3rd person camera isn't converted from .ess correctly + Bug #3421: Idling long enough while paralyzed sometimes causes character to get stuck + Bug #3423: Sleep interruption inside dungeons too agressive + Bug #3424: Pickpocketing sometimes won't work + Bug #3432: AiFollow / AiEscort durations handled incorrectly + Bug #3434: Dead NPC's and Creatures still contribute to sneak skill increases + Bug #3437: Weather-conditioned dialogue should not play in interiors + Bug #3439: Effects cast by summon stick around after their death + Bug #3440: Parallax maps looks weird + Bug #3443: Class graphic for custom class should be Acrobat + Bug #3446: OpenMW segfaults when using Atrayonis's "Anthology Solstheim: Tomb of the Snow Prince" mod + Bug #3448: After dispelled, invisibility icon is still displayed + Bug #3453: First couple of seconds of NPC speech is muted + Bug #3455: Portable house mods lock player and npc movement up exiting house. + Bug #3456: Equipping an item will undo dispel of constant effect invisibility + Bug #3458: Constant effect restore health doesn't work during Wait + Bug #3466: It is possible to stack multiple scroll effects of the same type + Bug #3471: When two mods delete the same references, many references are not disabled by the engine. + Bug #3473: 3rd person camera can be glitched + Feature #1424: NPC "Face" function + Feature #2974: Editor: Multiple Deletion of Subrecords + Feature #3044: Editor: Render path grid v2 + Feature #3362: Editor: Configurable key bindings + Feature #3375: Make sun / moon reflections weather dependent + Feature #3386: Editor: Edit pathgrid + 0.39.0 ------ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d00b2765..680f7146b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ endif() message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 39) +set(OPENMW_VERSION_MINOR 40) set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/opencs/model/prefs/modifiersetting.cpp b/apps/opencs/model/prefs/modifiersetting.cpp index d8bf84342..4f4fac248 100644 --- a/apps/opencs/model/prefs/modifiersetting.cpp +++ b/apps/opencs/model/prefs/modifiersetting.cpp @@ -15,6 +15,7 @@ namespace CSMPrefs ModifierSetting::ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) + , mButton(0) , mEditorActive(false) { } diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index 726566fdd..c56119deb 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -17,6 +17,7 @@ namespace CSMPrefs ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) + , mButton(0) , mEditorActive(false) , mEditorPos(0) { diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 394e6772d..f5b15cb55 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -74,7 +74,9 @@ osg::Vec3f CSVRender::InstanceMode::getSelectionCenter(const std::vector 0) + center /= objectCount; return center; } @@ -92,7 +94,7 @@ osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos) CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, QWidget *parent) : EditMode (worldspaceWidget, QIcon (":placeholder"), Mask_Reference, "Instance editing", parent), mSubMode (0), mSubModeId ("move"), mSelectionMode (0), mDragMode (DragMode_None), - mDragAxis (-1), mLocked (false) + mDragAxis (-1), mLocked (false), mUnitScaleDist(1) { } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 99d4a6011..7494d2b43 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -124,7 +124,7 @@ namespace MWClass int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); const ESM::MagicEffect *effect = store.get().find(index); - animation->addSpellCastGlow(effect); // TODO: Telekinesis glow should only be as long as the door animation + animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing } // make key id lowercase diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c92530259..637c24300 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1524,6 +1524,26 @@ bool CharacterController::updateWeaponState() return forcestateupdate; } +void CharacterController::updateAnimQueue() +{ + if(mAnimQueue.size() > 1) + { + if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) + { + mAnimation->disable(mAnimQueue.front().mGroup); + mAnimQueue.pop_front(); + + bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); + mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, + MWRender::Animation::BlendMask_All, false, + 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); + } + } + + if(!mAnimQueue.empty()) + mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); +} + void CharacterController::update(float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); @@ -1534,21 +1554,7 @@ void CharacterController::update(float duration) updateMagicEffects(); if(!cls.isActor()) - { - if(mAnimQueue.size() > 1) - { - if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) - { - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.pop_front(); - - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, - MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); - } - } - } + updateAnimQueue(); else if(!cls.getCreatureStats(mPtr).isDead()) { bool onground = world->isOnGround(mPtr); @@ -1816,19 +1822,8 @@ void CharacterController::update(float duration) { idlestate = (inwater ? CharState_IdleSwim : (sneak && !inJump ? CharState_IdleSneak : CharState_Idle)); } - else if(mAnimQueue.size() > 1) - { - if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) - { - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.pop_front(); - - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, - MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); - } - } + else + updateAnimQueue(); if (!mSkipAnim) { @@ -1994,9 +1989,10 @@ void CharacterController::unpersistAnimationState() mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; + bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); mAnimation->play(anim.mGroup, Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", complete, anim.mLoopCount); + "start", "stop", complete, anim.mLoopCount, loopfallback); } } @@ -2009,6 +2005,27 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int } else { + // If the given animation is a looped animation, is already playing + // and has not yet reached its Loop Stop key, make it the only animation + // in the queue, and retain the loop count from the animation that was + // already playing. This emulates observed behavior from the original + // engine and allows banners to animate correctly. + if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && + mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop start") >= 0) + { + float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop"); + + if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key + endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop"); + + if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) + { + mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, true); + mAnimQueue.resize(1); + return true; + } + } + count = std::max(count, 1); AnimationQueueEntry entry; @@ -2032,8 +2049,6 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int } else if(mode == 0) { - if (!mAnimQueue.empty()) - mAnimation->stopLooping(mAnimQueue.front().mGroup); mAnimQueue.resize(1); mAnimQueue.push_back(entry); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index d5dc5fe28..4661d6983 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -212,6 +212,8 @@ class CharacterController : public MWRender::Animation::TextKeyListener bool updateCreatureState(); void updateIdleStormState(bool inwater); + void updateAnimQueue(); + void updateHeadTracking(float duration); void updateMagicEffects(); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index e5614f3f8..386a4a53b 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -873,16 +873,6 @@ namespace MWRender addControllers(); } - void Animation::stopLooping(const std::string& groupname) - { - AnimStateMap::iterator stateiter = mStates.find(groupname); - if(stateiter != mStates.end()) - { - stateiter->second.mLoopCount = 0; - return; - } - } - void Animation::adjustSpeedMult(const std::string &groupname, float speedmult) { AnimStateMap::iterator state(mStates.find(groupname)); @@ -1023,35 +1013,33 @@ namespace MWRender { float targetTime; - if(state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) - goto handle_loop; - - targetTime = state.getTime() + timepassed; - if(textkey == textkeys.end() || textkey->first > targetTime) - { - if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) - updatePosition(state.getTime(), targetTime, movement); - state.setTime(std::min(targetTime, state.mStopTime)); - } - else + if (!state.shouldLoop()) { - if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) - updatePosition(state.getTime(), textkey->first, movement); - state.setTime(textkey->first); - } + targetTime = state.getTime() + timepassed; + if(textkey == textkeys.end() || textkey->first > targetTime) + { + if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) + updatePosition(state.getTime(), targetTime, movement); + state.setTime(std::min(targetTime, state.mStopTime)); + } + else + { + if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) + updatePosition(state.getTime(), textkey->first, movement); + state.setTime(textkey->first); + } - state.mPlaying = (state.getTime() < state.mStopTime); - timepassed = targetTime - state.getTime(); + state.mPlaying = (state.getTime() < state.mStopTime); + timepassed = targetTime - state.getTime(); - while(textkey != textkeys.end() && textkey->first <= state.getTime()) - { - handleTextKey(state, stateiter->first, textkey, textkeys); - ++textkey; + while(textkey != textkeys.end() && textkey->first <= state.getTime()) + { + handleTextKey(state, stateiter->first, textkey, textkeys); + ++textkey; + } } - - if(state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) + if(state.shouldLoop()) { - handle_loop: state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; @@ -1065,7 +1053,7 @@ namespace MWRender if(state.getTime() >= state.mLoopStopTime) break; - } + } if(timepassed <= 0.0f) break; @@ -1095,6 +1083,13 @@ namespace MWRender return movement; } + void Animation::setLoopingEnabled(const std::string &groupname, bool enabled) + { + AnimStateMap::iterator state(mStates.find(groupname)); + if(state != mStates.end()) + state->second.mLoopingEnabled = enabled; + } + void Animation::setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature) { osg::ref_ptr previousStateset; @@ -1192,7 +1187,7 @@ namespace MWRender int mLowestUnusedTexUnit; }; - void Animation::addSpellCastGlow(const ESM::MagicEffect *effect) + void Animation::addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration) { osg::Vec4f glowColor(1,1,1,1); glowColor.x() = effect->mData.mRed / 255.f; @@ -1207,10 +1202,10 @@ namespace MWRender if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { mGlowUpdater->setColor(glowColor); - mGlowUpdater->setDuration(1.5); // Glow length measured from original engine as about 1.5 seconds + mGlowUpdater->setDuration(glowDuration); } else - addGlow(mObjectRoot, glowColor, 1.5); + addGlow(mObjectRoot, glowColor, glowDuration); } } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index a837a26ae..089d0d85b 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -183,6 +183,7 @@ protected: float mSpeedMult; bool mPlaying; + bool mLoopingEnabled; size_t mLoopCount; AnimPriority mPriority; @@ -190,8 +191,8 @@ protected: bool mAutoDisable; AnimState() : mStartTime(0.0f), mLoopStartTime(0.0f), mLoopStopTime(0.0f), mStopTime(0.0f), - mTime(new float), mSpeedMult(1.0f), mPlaying(false), mLoopCount(0), - mPriority(0), mBlendMask(0), mAutoDisable(true) + mTime(new float), mSpeedMult(1.0f), mPlaying(false), mLoopingEnabled(true), + mLoopCount(0), mPriority(0), mBlendMask(0), mAutoDisable(true) { } ~AnimState(); @@ -204,6 +205,11 @@ protected: { *mTime = time; } + + bool shouldLoop() const + { + return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; + } }; typedef std::map AnimStateMap; AnimStateMap mStates; @@ -354,7 +360,9 @@ public: void removeEffect (int effectId); void getLoopingEffects (std::vector& out) const; - void addSpellCastGlow(const ESM::MagicEffect *effect); + // Add a spell casting glow to an object. From measuring video taken from the original engine, + // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. + void addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration = 1.5); virtual void updatePtr(const MWWorld::Ptr &ptr); @@ -389,10 +397,6 @@ public: float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops, bool loopfallback=false); - /** If the given animation group is currently playing, set its remaining loop count to '0'. - */ - void stopLooping(const std::string& groupName); - /** Adjust the speed multiplier of an already playing animation. */ void adjustSpeedMult (const std::string& groupname, float speedmult); @@ -432,6 +436,8 @@ public: virtual osg::Vec3f runAnimation(float duration); + void setLoopingEnabled(const std::string &groupname, bool enabled); + /// This is typically called as part of runAnimation, but may be called manually if needed. void updateEffects(float duration); diff --git a/components/files/escape.cpp b/components/files/escape.cpp index 9a795b95f..c5d2c041e 100644 --- a/components/files/escape.cpp +++ b/components/files/escape.cpp @@ -9,7 +9,7 @@ namespace Files const int escape_hash_filter::sEscapeIdentifier = 'a'; const int escape_hash_filter::sHashIdentifier = 'h'; - escape_hash_filter::escape_hash_filter() : mNext(), mSeenNonWhitespace(false), mFinishLine(false) + escape_hash_filter::escape_hash_filter() : mNext(), mPrevious(), mSeenNonWhitespace(false), mFinishLine(false) { } @@ -137,4 +137,4 @@ namespace Files return istream; } -} \ No newline at end of file +} diff --git a/components/misc/messageformatparser.cpp b/components/misc/messageformatparser.cpp index 7c0c978b2..bfd8dd562 100644 --- a/components/misc/messageformatparser.cpp +++ b/components/misc/messageformatparser.cpp @@ -2,6 +2,8 @@ namespace Misc { + MessageFormatParser::~MessageFormatParser() {} + void MessageFormatParser::process(const std::string& m) { for (unsigned int i = 0; i < m.size(); ++i) diff --git a/components/misc/messageformatparser.hpp b/components/misc/messageformatparser.hpp index 48faff714..c12b9352a 100644 --- a/components/misc/messageformatparser.hpp +++ b/components/misc/messageformatparser.hpp @@ -19,6 +19,8 @@ namespace Misc virtual void visitedCharacter(char c) = 0; public: + virtual ~MessageFormatParser(); + virtual void process(const std::string& message); }; }