From e654a52b70a006e335aea2ccefde13a6f22a1405 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Sun, 29 Dec 2019 15:53:44 +0300 Subject: [PATCH 01/25] More NIF adjustments Constant interpolation support --- components/nif/data.cpp | 18 +++-- components/nif/niffile.cpp | 17 +---- components/nif/niffile.hpp | 68 +++++++++++++++--- components/nif/nifkey.hpp | 61 ++++++++-------- components/nif/nifstream.cpp | 17 +++++ components/nif/nifstream.hpp | 48 ++++++++++++- components/nif/node.hpp | 2 +- components/nif/property.cpp | 2 +- components/nif/recordptr.hpp | 1 + components/nifosg/controller.hpp | 117 ++++++++++++++++--------------- components/nifosg/nifloader.cpp | 14 ++-- components/nifosg/particle.hpp | 1 - 12 files changed, 236 insertions(+), 130 deletions(-) diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 4e1487f69..cde8e5876 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -35,16 +35,16 @@ void ShapeData::read(NIFStream *nif) { int verts = nif->getUShort(); - if(nif->getInt()) + if (nif->getBoolean()) nif->getVector3s(vertices, verts); - if(nif->getInt()) + if (nif->getBoolean()) nif->getVector3s(normals, verts); center = nif->getVector3(); radius = nif->getFloat(); - if(nif->getInt()) + if (nif->getBoolean()) nif->getVector4s(colors, verts); // Only the first 6 bits are used as a count. I think the rest are @@ -120,7 +120,7 @@ void NiAutoNormalParticlesData::read(NIFStream *nif) particleRadius = nif->getFloat(); activeCount = nif->getUShort(); - if(nif->getInt()) + if (nif->getBoolean()) { // Particle sizes nif->getFloats(sizes, vertices.size()); @@ -131,7 +131,7 @@ void NiRotatingParticlesData::read(NIFStream *nif) { NiAutoNormalParticlesData::read(nif); - if(nif->getInt()) + if (nif->getBoolean()) { // Rotation quaternions. nif->getQuaternions(rotations, vertices.size()); @@ -176,7 +176,7 @@ void NiPixelData::read(NIFStream *nif) numberOfMipmaps = nif->getUInt(); - // Bytes per pixel, should be bpp * 8 + // Bytes per pixel, should be bpp / 8 /* int bytes = */ nif->getUInt(); for(unsigned int i=0; igetInt(); // -1 bones.resize(boneNum); - for(int i=0;igetMatrix3(); bi.trafo.pos = nif->getVector3(); bi.trafo.scale = nif->getFloat(); @@ -267,7 +265,7 @@ void NiKeyframeData::read(NIFStream *nif) { mRotations = std::make_shared(); mRotations->read(nif); - if(mRotations->mInterpolationType == Vector3KeyMap::sXYZInterpolation) + if(mRotations->mInterpolationType == Vector3KeyMap::XYZ) { //Chomp unused float nif->getFloat(); diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index d4f1203cc..bced4d8e2 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -9,9 +9,7 @@ namespace Nif /// Open a NIF stream. The name is used for error messages. NIFFile::NIFFile(Files::IStreamPtr stream, const std::string &name) - : ver(0) - , filename(name) - , mUseSkinning(false) + : filename(name) { parse(stream); } @@ -139,27 +137,18 @@ void NIFFile::parse(Files::IStreamPtr stream) // Check the header string std::string head = nif.getVersionString(); if(head.compare(0, 22, "NetImmerse File Format") != 0) - fail("Invalid NIF header: " + head); + fail("Invalid NIF header: " + head); // Get BCD version ver = nif.getUInt(); // 4.0.0.0 is an older, practically identical version of the format. // It's not used by Morrowind assets but Morrowind supports it. - if(ver != 0x04000000 && ver != VER_MW) + if(ver != VER_4_0_0_0 && ver != VER_MW) fail("Unsupported NIF version: " + printVersion(ver)); // Number of records size_t recNum = nif.getInt(); records.resize(recNum); - /* The format for 10.0.1.0 seems to be a bit different. After the - header, it contains the number of records, r (int), just like - 4.0.0.2, but following that it contains a short x, followed by x - strings. Then again by r shorts, one for each record, giving - which of the above strings to use to identify the record. After - this follows two ints (zero?) and then the record data. However - we do not support or plan to support other versions yet. - */ - for(size_t i = 0;i < recNum;i++) { Record *r = nullptr; diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 15001f511..a85e46ea5 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -26,21 +26,27 @@ struct File virtual size_t numRoots() const = 0; + virtual std::string getString(size_t index) const = 0; + virtual void setUseSkinning(bool skinning) = 0; virtual bool getUseSkinning() const = 0; virtual std::string getFilename() const = 0; + + virtual unsigned int getVersion() const = 0; + + virtual unsigned int getUserVersion() const = 0; + + virtual unsigned int getBethVersion() const = 0; }; class NIFFile final : public File { - enum NIFVersion { - VER_MW = 0x04000002 // Morrowind NIFs - }; - - /// Nif file version - unsigned int ver; + /// File version, user version, Bethesda version + unsigned int ver = 0; + unsigned int userVer = 0; + unsigned int bethVer = 0; /// File name, used for error messages and opening the file std::string filename; @@ -51,7 +57,10 @@ class NIFFile final : public File /// Root list. This is a select portion of the pointers from records std::vector roots; - bool mUseSkinning; + /// String table + std::vector strings; + + bool mUseSkinning = false; /// Parse the file void parse(Files::IStreamPtr stream); @@ -66,6 +75,34 @@ class NIFFile final : public File void operator = (NIFFile const &); public: + enum NIFVersion + { + // Feature-relevant + VER_4_1_0_0 = 0x04010000, // 1-byte booleans (previously 4-byte) + VER_5_0_0_1 = 0x05000001, // Optimized record type listings + VER_5_0_0_6 = 0x05000006, // Record groups + VER_10_0_1_8 = 0x0A000108, // The last version without user version + VER_20_1_0_1 = 0x14010001, // String tables + VER_20_2_0_5 = 0x14020005, // Record sizes + // Game-relevant + VER_4_0_0_0 = 0x04000000, // Freedom Force NIFs, supported by Morrowind + VER_MW = 0x04000002, // 4.0.0.2. Morrowind and Freedom Force NIFs + VER_4_2_1_0 = 0x04020100, // Used in Civ4 and Dark Age of Camelot + VER_CI = 0x04020200, // 4.2.2.0. Main Culpa Innata NIF version, also used in Civ4 + VER_ZT2 = 0x0A000100, // 10.0.1.0. Main Zoo Tycoon 2 NIF version, also used in Oblivion and Civ4 + VER_OB_OLD = 0x0A000102, // 10.0.1.2. Main older Oblivion NIF version + VER_GAMEBRYO = 0x0A010000, // 10.1.0.0. Lots of games use it. The first version that has Gamebryo File Format header. + VER_10_2_0_0 = 0x0A020000, // Lots of games use this version as well. + VER_CIV4 = 0x14000004, // 20.0.0.4. Main Civilization IV NIF version. + VER_OB = 0x14000005, // 20.0.0.5. Main Oblivion NIF version + VER_BGS = 0x14020007 // 20.2.0.7. Main Fallout 3/4/76/New Vegas and Skyrim/SkyrimSE NIF version. + }; + enum BethVersion + { + BETHVER_FO3 = 34, // Fallout 3 + BETHVER_FO4 = 130 // Fallout 4 + }; + /// Used if file parsing fails void fail(const std::string &msg) const { @@ -101,6 +138,12 @@ public: /// Number of roots size_t numRoots() const override { return roots.size(); } + /// Get a given string from the file's string table + std::string getString(size_t index) const override + { + return strings.at(index); + } + /// Set whether there is skinning contained in this NIF file. /// @note This is just a hint for users of the NIF file and has no effect on the loading procedure. void setUseSkinning(bool skinning) override; @@ -109,8 +152,17 @@ public: /// Get the name of the file std::string getFilename() const override { return filename; } + + /// Get the version of the NIF format used + unsigned int getVersion() const override { return ver; } + + /// Get the user version of the NIF format used + unsigned int getUserVersion() const override { return userVer; } + + /// Get the Bethesda version of the NIF format used + unsigned int getBethVersion() const override { return bethVer; } }; -typedef std::shared_ptr NIFFilePtr; +using NIFFilePtr = std::shared_ptr; diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index 9238a6548..3e03b4b1f 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -26,34 +26,37 @@ struct KeyT { float mContinuity; // Only for TBC interpolation */ }; -typedef KeyT FloatKey; -typedef KeyT Vector3Key; -typedef KeyT Vector4Key; -typedef KeyT QuaternionKey; +using FloatKey = KeyT; +using Vector3Key = KeyT; +using Vector4Key = KeyT; +using QuaternionKey = KeyT; template struct KeyMapT { - typedef std::map< float, KeyT > MapType; + using MapType = std::map>; - typedef T ValueType; - typedef KeyT KeyType; + using ValueType = T; + using KeyType = KeyT; - static const unsigned int sLinearInterpolation = 1; - static const unsigned int sQuadraticInterpolation = 2; - static const unsigned int sTBCInterpolation = 3; - static const unsigned int sXYZInterpolation = 4; + enum InterpolationType + { + Unknown = 0, + Linear = 1, + Quadratic = 2, + TBC = 3, + XYZ = 4, + Constant = 5 + }; - unsigned int mInterpolationType; + unsigned int mInterpolationType = Linear; MapType mKeys; - KeyMapT() : mInterpolationType(sLinearInterpolation) {} - //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) void read(NIFStream *nif, bool force=false) { assert(nif); - mInterpolationType = 0; + mInterpolationType = Unknown; size_t count = nif->getUInt(); if(count == 0 && !force) @@ -66,7 +69,7 @@ struct KeyMapT { KeyT key; NIFStream &nifReference = *nif; - if(mInterpolationType == sLinearInterpolation) + if (mInterpolationType == Linear || mInterpolationType == Constant) { for(size_t i = 0;i < count;i++) { @@ -75,7 +78,7 @@ struct KeyMapT { mKeys[time] = key; } } - else if(mInterpolationType == sQuadraticInterpolation) + else if (mInterpolationType == Quadratic) { for(size_t i = 0;i < count;i++) { @@ -84,7 +87,7 @@ struct KeyMapT { mKeys[time] = key; } } - else if(mInterpolationType == sTBCInterpolation) + else if (mInterpolationType == TBC) { for(size_t i = 0;i < count;i++) { @@ -98,7 +101,7 @@ struct KeyMapT { // Eats a floating point number, then // Re-runs the read function 3 more times. // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. - else if(mInterpolationType == sXYZInterpolation) + else if(mInterpolationType == XYZ) { //Don't try to read XYZ keys into the wrong part if ( count != 1 ) @@ -109,7 +112,7 @@ struct KeyMapT { nif->file->fail(error.str()); } } - else if (0 == mInterpolationType) + else if (mInterpolationType == Unknown) { if (count != 0) nif->file->fail("Interpolation type 0 doesn't work with keys"); @@ -149,15 +152,17 @@ private: /*key.mContinuity = */nif.getFloat(); } }; -typedef KeyMapT FloatKeyMap; -typedef KeyMapT Vector3KeyMap; -typedef KeyMapT Vector4KeyMap; -typedef KeyMapT QuaternionKeyMap; +using FloatKeyMap = KeyMapT; +using Vector3KeyMap = KeyMapT; +using Vector4KeyMap = KeyMapT; +using QuaternionKeyMap = KeyMapT; +using ByteKeyMap = KeyMapT; -typedef std::shared_ptr FloatKeyMapPtr; -typedef std::shared_ptr Vector3KeyMapPtr; -typedef std::shared_ptr Vector4KeyMapPtr; -typedef std::shared_ptr QuaternionKeyMapPtr; +using FloatKeyMapPtr = std::shared_ptr; +using Vector3KeyMapPtr = std::shared_ptr; +using Vector4KeyMapPtr = std::shared_ptr; +using QuaternionKeyMapPtr = std::shared_ptr; +using ByteKeyMapPtr = std::shared_ptr; } // Namespace #endif //#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 3811d05ee..6e00cbd13 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -24,4 +24,21 @@ namespace Nif t.scale = getFloat(); return t; } + + ///Currently specific for 4.0.0.2 and earlier + bool NIFStream::getBoolean() + { + return !!getInt(); + } + + ///Read in a string, either from the string table using the index (currently absent) or from the stream using the specified length + std::string NIFStream::getString() + { + return getSizedString(); + } + + // Convenience utility functions: get the versions of the currently read file + unsigned int NIFStream::getVersion() { return file->getVersion(); } + unsigned int NIFStream::getUserVersion() { return file->getBethVersion(); } + unsigned int NIFStream::getBethVersion() { return file->getBethVersion(); } } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index b685c5662..7ecd66084 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -155,8 +155,16 @@ public: Transformation getTrafo(); + bool getBoolean(); + + std::string getString(); + + unsigned int getVersion(); + unsigned int getUserVersion(); + unsigned int getBethVersion(); + ///Read in a string of the given length - std::string getString(size_t length) + std::string getSizedString(size_t length) { std::vector str(length + 1, 0); @@ -165,11 +173,19 @@ public: return str.data(); } ///Read in a string of the length specified in the file - std::string getString() + std::string getSizedString() { size_t size = readLittleEndianType(inp); - return getString(size); + return getSizedString(size); } + + ///Specific to Bethesda headers, uses a byte for length + std::string getExportString() + { + size_t size = static_cast(readLittleEndianType(inp)); + return getSizedString(size); + } + ///This is special since the version string doesn't start with a number, and ends with "\n" std::string getVersionString() { @@ -190,6 +206,18 @@ public: readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } + void getInts(std::vector &vec, size_t size) + { + vec.resize(size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + } + + void getUInts(std::vector &vec, size_t size) + { + vec.resize(size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + } + void getVector2s(std::vector &vec, size_t size) { vec.resize(size); @@ -217,6 +245,20 @@ public: for (size_t i = 0;i < quat.size();i++) quat[i] = getQuaternion(); } + + void getStrings(std::vector &vec, size_t size) + { + vec.resize(size); + for (size_t i = 0; i < vec.size(); i++) + vec[i] = getString(); + } + /// We need to use this when the string table isn't actually initialized. + void getSizedStrings(std::vector &vec, size_t size) + { + vec.resize(size); + for (size_t i = 0; i < vec.size(); i++) + vec[i] = getSizedString(); + } }; } diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 5e5f445cf..50221784c 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -24,7 +24,7 @@ class Node : public Named { public: // Node flags. Interpretation depends somewhat on the type of node. - int flags; + unsigned int flags; Transformation trafo; osg::Vec3f velocity; // Unused? Might be a run-time game state PropertyList props; diff --git a/components/nif/property.cpp b/components/nif/property.cpp index 1398326be..de30e7da9 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -14,7 +14,7 @@ void Property::read(NIFStream *nif) void NiTexturingProperty::Texture::read(NIFStream *nif) { - inUse = !!nif->getInt(); + inUse = nif->getBoolean(); if(!inUse) return; texture.read(nif); diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index d165111b8..478ecfdbb 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -167,6 +167,7 @@ using NiParticleModifierPtr = RecordPtrT; using NodeList = RecordListT; using PropertyList = RecordListT; +using ExtraList = RecordListT; using NiSourceTextureList = RecordListT; } // Namespace diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index a16bb6b71..85da77eda 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -35,17 +35,33 @@ namespace NifOsg { // interpolation of keyframes - template + template class ValueInterpolator { - public: - typedef typename MapT::ValueType ValueT; - - ValueInterpolator() - : mDefaultVal(ValueT()) + typename MapT::MapType::const_iterator retrieveKey(float time) const { + // retrieve the current position in the map, optimized for the most common case + // where time moves linearly along the keyframe track + if (mLastHighKey != mKeys->mKeys.end()) + { + if (time > mLastHighKey->first) + { + // try if we're there by incrementing one + ++mLastLowKey; + ++mLastHighKey; + } + if (mLastHighKey != mKeys->mKeys.end() && time >= mLastLowKey->first && time <= mLastHighKey->first) + return mLastHighKey; + } + + return mKeys->mKeys.lower_bound(time); } + public: + using ValueT = typename MapT::ValueType; + + ValueInterpolator() = default; + ValueInterpolator(std::shared_ptr keys, ValueT defaultVal = ValueT()) : mKeys(keys) , mDefaultVal(defaultVal) @@ -67,44 +83,21 @@ namespace NifOsg if(time <= keys.begin()->first) return keys.begin()->second.mValue; - // retrieve the current position in the map, optimized for the most common case - // where time moves linearly along the keyframe track - typename MapT::MapType::const_iterator it = mLastHighKey; - if (mLastHighKey != keys.end()) - { - if (time > mLastHighKey->first) - { - // try if we're there by incrementing one - ++mLastLowKey; - ++mLastHighKey; - it = mLastHighKey; - } - if (mLastHighKey == keys.end() || (time < mLastLowKey->first || time > mLastHighKey->first)) - it = keys.lower_bound(time); // still not there, reorient by performing lower_bound check on the whole map - } - else - it = keys.lower_bound(time); + typename MapT::MapType::const_iterator it = retrieveKey(time); // now do the actual interpolation if (it != keys.end()) { - float aTime = it->first; - const typename MapT::KeyType* aKey = &it->second; - // cache for next time mLastHighKey = it; + mLastLowKey = --it; - typename MapT::MapType::const_iterator last = --it; - mLastLowKey = last; - float aLastTime = last->first; - const typename MapT::KeyType* aLastKey = &last->second; + float a = (time - mLastLowKey->first) / (mLastHighKey->first - mLastLowKey->first); - float a = (time - aLastTime) / (aTime - aLastTime); - - return InterpolationFunc()(aLastKey->mValue, aKey->mValue, a); + return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType); } - else - return keys.rbegin()->second.mValue; + + return keys.rbegin()->second.mValue; } bool empty() const @@ -113,36 +106,44 @@ namespace NifOsg } private: + template + ValueType interpolate(const Nif::KeyT& a, const Nif::KeyT& b, float fraction, unsigned int type) const + { + switch (type) + { + case 5: + return fraction > 0.5f ? b.mValue : a.mValue; + default: + return a.mValue + ((b.mValue - a.mValue) * fraction); + } + } + osg::Quat interpolate(const Nif::KeyT& a, const Nif::KeyT& b, float fraction, unsigned int type) const + { + switch (type) + { + case 5: + return fraction > 0.5f ? b.mValue : a.mValue; + default: + { + osg::Quat result; + result.slerp(fraction, a.mValue, b.mValue); + return result; + } + } + } + mutable typename MapT::MapType::const_iterator mLastLowKey; mutable typename MapT::MapType::const_iterator mLastHighKey; std::shared_ptr mKeys; - ValueT mDefaultVal; + ValueT mDefaultVal = ValueT(); }; - struct LerpFunc - { - template - inline ValueType operator()(const ValueType& a, const ValueType& b, float fraction) - { - return a + ((b - a) * fraction); - } - }; - - struct QuaternionSlerpFunc - { - inline osg::Quat operator()(const osg::Quat& a, const osg::Quat& b, float fraction) - { - osg::Quat result; - result.slerp(fraction, a, b); - return result; - } - }; - - typedef ValueInterpolator QuaternionInterpolator; - typedef ValueInterpolator FloatInterpolator; - typedef ValueInterpolator Vec3Interpolator; + using QuaternionInterpolator = ValueInterpolator; + using FloatInterpolator = ValueInterpolator; + using Vec3Interpolator = ValueInterpolator; + using Vec4Interpolator = ValueInterpolator; class ControllerFunction : public SceneUtil::ControllerFunction { diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 2fd42e77a..50c1c6627 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -184,14 +184,16 @@ namespace NifOsg { public: /// @param filename used for warning messages. - LoaderImpl(const std::string& filename) - : mFilename(filename), mFirstRootTextureIndex(-1), mFoundFirstRootTexturingProperty(false) + LoaderImpl(const std::string& filename, unsigned int ver, unsigned int userver, unsigned int bethver) + : mFilename(filename), mVersion(ver), mUserVersion(userver), mBethVersion(bethver) { } std::string mFilename; - size_t mFirstRootTextureIndex; - bool mFoundFirstRootTexturingProperty; + unsigned int mVersion, mUserVersion, mBethVersion; + + size_t mFirstRootTextureIndex = -1; + bool mFoundFirstRootTexturingProperty = false; static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target) { @@ -1846,13 +1848,13 @@ namespace NifOsg osg::ref_ptr Loader::load(Nif::NIFFilePtr file, Resource::ImageManager* imageManager) { - LoaderImpl impl(file->getFilename()); + LoaderImpl impl(file->getFilename(), file->getVersion(), file->getUserVersion(), file->getBethVersion()); return impl.load(file, imageManager); } void Loader::loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target) { - LoaderImpl impl(kf->getFilename()); + LoaderImpl impl(kf->getFilename(), kf->getVersion(), kf->getUserVersion(), kf->getBethVersion()); impl.loadKf(kf, target); } diff --git a/components/nifosg/particle.hpp b/components/nifosg/particle.hpp index 4b6891fed..e159914c7 100644 --- a/components/nifosg/particle.hpp +++ b/components/nifosg/particle.hpp @@ -151,7 +151,6 @@ namespace NifOsg float mCachedDefaultSize; }; - typedef ValueInterpolator Vec4Interpolator; class ParticleColorAffector : public osgParticle::Operator { public: From c61f64ae8605b4ae0bb8cad51d4e020095d54c9f Mon Sep 17 00:00:00 2001 From: capostrophic Date: Mon, 30 Dec 2019 13:33:53 +0300 Subject: [PATCH 02/25] Fix tests --- apps/openmw_test_suite/nifloader/testbulletnifloader.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index b02b8b9ef..3044bf270 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -266,9 +266,13 @@ namespace MOCK_CONST_METHOD0(numRecords, std::size_t ()); MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t)); MOCK_CONST_METHOD0(numRoots, std::size_t ()); + MOCK_CONST_METHOD1(getString, std::string (std::size_t)); MOCK_METHOD1(setUseSkinning, void (bool)); MOCK_CONST_METHOD0(getUseSkinning, bool ()); MOCK_CONST_METHOD0(getFilename, std::string ()); + MOCK_CONST_METHOD0(getVersion, unsigned int ()); + MOCK_CONST_METHOD0(getUserVersion, unsigned int ()); + MOCK_CONST_METHOD0(getBethVersion, unsigned int ()); }; struct RecordMock : Nif::Record From 32caab663f5e0ed00781ec949465407e25d0deaa Mon Sep 17 00:00:00 2001 From: capostrophic Date: Tue, 31 Dec 2019 13:38:57 +0300 Subject: [PATCH 03/25] Enumerate interpolation types properly --- components/nif/data.cpp | 2 +- components/nif/nifkey.hpp | 39 ++++++++++++++++---------------- components/nifosg/controller.hpp | 4 ++-- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/components/nif/data.cpp b/components/nif/data.cpp index cde8e5876..828f5c368 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -265,7 +265,7 @@ void NiKeyframeData::read(NIFStream *nif) { mRotations = std::make_shared(); mRotations->read(nif); - if(mRotations->mInterpolationType == Vector3KeyMap::XYZ) + if(mRotations->mInterpolationType == InterpolationType_XYZ) { //Chomp unused float nif->getFloat(); diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index 3e03b4b1f..333d8a7cf 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -13,6 +13,16 @@ namespace Nif { +enum InterpolationType +{ + InterpolationType_Unknown = 0, + InterpolationType_Linear = 1, + InterpolationType_Quadratic = 2, + InterpolationType_TBC = 3, + InterpolationType_XYZ = 4, + InterpolationType_Constant = 5 +}; + template struct KeyT { T mValue; @@ -38,17 +48,7 @@ struct KeyMapT { using ValueType = T; using KeyType = KeyT; - enum InterpolationType - { - Unknown = 0, - Linear = 1, - Quadratic = 2, - TBC = 3, - XYZ = 4, - Constant = 5 - }; - - unsigned int mInterpolationType = Linear; + unsigned int mInterpolationType = InterpolationType_Linear; MapType mKeys; //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) @@ -56,7 +56,7 @@ struct KeyMapT { { assert(nif); - mInterpolationType = Unknown; + mInterpolationType = InterpolationType_Unknown; size_t count = nif->getUInt(); if(count == 0 && !force) @@ -69,7 +69,8 @@ struct KeyMapT { KeyT key; NIFStream &nifReference = *nif; - if (mInterpolationType == Linear || mInterpolationType == Constant) + if (mInterpolationType == InterpolationType_Linear + || mInterpolationType == InterpolationType_Constant) { for(size_t i = 0;i < count;i++) { @@ -78,7 +79,7 @@ struct KeyMapT { mKeys[time] = key; } } - else if (mInterpolationType == Quadratic) + else if (mInterpolationType == InterpolationType_Quadratic) { for(size_t i = 0;i < count;i++) { @@ -87,7 +88,7 @@ struct KeyMapT { mKeys[time] = key; } } - else if (mInterpolationType == TBC) + else if (mInterpolationType == InterpolationType_TBC) { for(size_t i = 0;i < count;i++) { @@ -97,11 +98,11 @@ struct KeyMapT { } } //XYZ keys aren't actually read here. - //data.hpp sees that the last type read was sXYZInterpolation and: + //data.hpp sees that the last type read was InterpolationType_XYZ and: // Eats a floating point number, then // Re-runs the read function 3 more times. - // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. - else if(mInterpolationType == XYZ) + // When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ. + else if(mInterpolationType == InterpolationType_XYZ) { //Don't try to read XYZ keys into the wrong part if ( count != 1 ) @@ -112,7 +113,7 @@ struct KeyMapT { nif->file->fail(error.str()); } } - else if (mInterpolationType == Unknown) + else if (mInterpolationType == InterpolationType_Unknown) { if (count != 0) nif->file->fail("Interpolation type 0 doesn't work with keys"); diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 85da77eda..c9bda2e62 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -111,7 +111,7 @@ namespace NifOsg { switch (type) { - case 5: + case Nif::InterpolationType_Constant: return fraction > 0.5f ? b.mValue : a.mValue; default: return a.mValue + ((b.mValue - a.mValue) * fraction); @@ -121,7 +121,7 @@ namespace NifOsg { switch (type) { - case 5: + case Nif::InterpolationType_Constant: return fraction > 0.5f ? b.mValue : a.mValue; default: { From f234d532691c115708dc3557c603f54e59d69e93 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Thu, 2 Jan 2020 13:09:54 +0300 Subject: [PATCH 04/25] Don't use double negation --- components/nif/controlled.cpp | 2 +- components/nif/nifstream.cpp | 2 +- components/nif/node.hpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/nif/controlled.cpp b/components/nif/controlled.cpp index 51ccf8541..9b7c9319b 100644 --- a/components/nif/controlled.cpp +++ b/components/nif/controlled.cpp @@ -9,7 +9,7 @@ namespace Nif { Named::read(nif); - external = !!nif->getChar(); + external = nif->getChar() != 0; if(external) filename = nif->getString(); else diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 6e00cbd13..4ecb0e373 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -28,7 +28,7 @@ namespace Nif ///Currently specific for 4.0.0.2 and earlier bool NIFStream::getBoolean() { - return !!getInt(); + return getInt() != 0; } ///Read in a string, either from the string table using the index (currently absent) or from the stream using the specified length diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 50221784c..71a0a93ca 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -44,7 +44,7 @@ public: velocity = nif->getVector3(); props.read(nif); - hasBounds = !!nif->getInt(); + hasBounds = nif->getBoolean(); if(hasBounds) { nif->getInt(); // always 1 From 3704acf85719a3dbac57f4f72a056f11b19d9220 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 2 Jan 2020 11:48:01 +0400 Subject: [PATCH 05/25] Unify streams usage to support non-ASCII paths (bug #5239) --- CHANGELOG.md | 1 + apps/essimporter/importer.cpp | 3 ++- apps/opencs/model/doc/document.cpp | 26 ++++++++++++++------------ components/detournavigator/debug.cpp | 7 ++++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a2632b9..ebda3d4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,7 @@ Bug #5223: Bow replacement during attack animation removes attached arrow Bug #5226: Reputation should be capped Bug #5229: Crash if mesh controller node has no data node + Bug #5239: OpenMW-CS does not support non-ASCII characters in path names Feature #1774: Handle AvoidNode Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index a54c13334..36512579c 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -349,7 +350,7 @@ namespace ESSImport writer.setFormat (ESM::SavedGame::sCurrentFormat); - std::ofstream stream(mOutFile.c_str(), std::ios::binary); + boost::filesystem::ofstream stream(boost::filesystem::path(mOutFile), std::ios::out | std::ios::binary); // all unused writer.setVersion(0); writer.setType(0); diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 03693ddfe..3a20555d1 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -1,7 +1,6 @@ #include "document.hpp" #include -#include #include #include @@ -289,19 +288,22 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, if (mNew || !boost::filesystem::exists (mProjectPath)) { - boost::filesystem::path customFiltersPath (configuration.getUserDataPath()); - customFiltersPath /= "defaultfilters"; + boost::filesystem::path filtersPath (configuration.getUserDataPath() / "defaultfilters"); - std::ofstream destination (mProjectPath.string().c_str(), std::ios::binary); + boost::filesystem::ofstream destination(mProjectPath, std::ios::out | std::ios::binary); + if (!destination.is_open()) + throw std::runtime_error("Can not create project file: " + mProjectPath.string()); + destination.exceptions(std::ios::failbit | std::ios::badbit); - if (boost::filesystem::exists (customFiltersPath)) - { - destination << std::ifstream(customFiltersPath.string().c_str(), std::ios::binary).rdbuf(); - } - else - { - destination << std::ifstream(std::string(mResDir.string() + "/defaultfilters").c_str(), std::ios::binary).rdbuf(); - } + if (!boost::filesystem::exists (filtersPath)) + filtersPath = mResDir / "defaultfilters"; + + boost::filesystem::ifstream source(filtersPath, std::ios::in | std::ios::binary); + if (!source.is_open()) + throw std::runtime_error("Can not read filters file: " + filtersPath.string()); + source.exceptions(std::ios::failbit | std::ios::badbit); + + destination << source.rdbuf(); } if (mNew) diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp index 0ddf002d9..c3d67b184 100644 --- a/components/detournavigator/debug.cpp +++ b/components/detournavigator/debug.cpp @@ -4,14 +4,15 @@ #include -#include +#include +#include namespace DetourNavigator { void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision) { const auto path = pathPrefix + "recastmesh" + revision + ".obj"; - std::ofstream file(path); + boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out); if (!file.is_open()) throw NavigatorException("Open file failed: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); @@ -64,7 +65,7 @@ namespace DetourNavigator }; const auto path = pathPrefix + "all_tiles_navmesh" + revision + ".bin"; - std::ofstream file(path, std::ios::binary); + boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out | std::ios::binary); if (!file.is_open()) throw NavigatorException("Open file failed: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); From b6899a821b511a286ef93896c4af150a5018fbfd Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 3 Jan 2020 08:45:53 +0400 Subject: [PATCH 06/25] Improve BetaComment handling (feature #4129) --- CHANGELOG.md | 1 + apps/openmw/mwscript/miscextensions.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a2632b9..f58ce5789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -196,6 +196,7 @@ Feature #3980: In-game option to disable controller Feature #3999: Shift + Double Click should maximize/restore menu size Feature #4001: Toggle sneak controller shortcut + Feature #4129: Beta Comment to File Feature #4209: Editor: Faction rank sub-table Feature #4255: Handle broken RepairedOnMe script function Feature #4316: Implement RaiseRank/LowerRank functions properly diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index d40ad769d..ada1af59e 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1,10 +1,13 @@ #include "miscextensions.hpp" #include +#include #include #include +#include + #include #include #include @@ -1222,6 +1225,11 @@ namespace MWScript std::stringstream msg; + msg << "Report time: "; + + std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + msg << std::put_time(std::gmtime(¤tTime), "%d.%m.%Y %T UTC") << std::endl; + msg << "Content file: "; if (!ptr.getCellRef().hasContentFile()) @@ -1263,6 +1271,8 @@ namespace MWScript --arg0; } + Log(Debug::Warning) << "\n" << msg.str(); + runtime.getContext().report(msg.str()); } }; From 0467e8ee15831c988e23cb3386cba1e0d97b2712 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Sat, 4 Jan 2020 01:26:32 +0300 Subject: [PATCH 07/25] Avoid heap corruption while reading SCVR (bug #4680) --- CHANGELOG.md | 1 + components/esm/loadscpt.cpp | 34 ++++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a2632b9..b34443809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Bug #4600: Crash when no sound output is available or --no-sound is used. Bug #4639: Black screen after completing first mages guild mission + training Bug #4650: Focus is lost after pressing ESC in confirmation dialog inside savegame dialog + Bug #4680: Heap corruption on faulty esp Bug #4701: PrisonMarker record is not hardcoded like other markers Bug #4703: Editor: it's possible to preview levelled list records Bug #4705: Editor: unable to open exterior cell views from Instances table diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 10a91d2e2..8db284e1f 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -20,7 +20,7 @@ namespace ESM int left = esm.getSubSize(); if (left < s) esm.fail("SCVR string list is smaller than specified"); - esm.getExact(&tmp[0], s); + esm.getExact(tmp.data(), s); if (left > s) esm.skip(left-s); // skip the leftover junk @@ -29,37 +29,47 @@ namespace ESM // The tmp buffer is a null-byte separated string list, we // just have to pick out one string at a time. - char* str = &tmp[0]; + char* str = tmp.data(); if (!str && mVarNames.size() > 0) { Log(Debug::Warning) << "SCVR with no variable names"; return; } + // Support '\r' terminated strings like vanilla. See Bug #1324. + std::replace(tmp.begin(), tmp.end(), '\r', '\0'); + // Avoid heap corruption + if (!tmp.empty() && tmp[tmp.size()-1] != '\0') + { + tmp.emplace_back('\0'); + std::stringstream ss; + ss << "Malformed string table"; + ss << "\n File: " << esm.getName(); + ss << "\n Record: " << esm.getContext().recName.toString(); + ss << "\n Subrecord: " << "SCVR"; + ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); + Log(Debug::Verbose) << ss.str(); + } + for (size_t i = 0; i < mVarNames.size(); i++) { - // Support '\r' terminated strings like vanilla. See Bug #1324. - char *termsym = strchr(str, '\r'); - if(termsym) *termsym = '\0'; mVarNames[i] = std::string(str); str += mVarNames[i].size() + 1; - - if (str - &tmp[0] > s) + if (static_cast(str - tmp.data()) > tmp.size()) { - // Apparently SCVR subrecord is not used and variable names are - // determined on the fly from the script text. Therefore don't throw - // an exeption, just log an error and continue. + // SCVR subrecord is unused and variable names are determined + // from the script source, so an overflow is not fatal. std::stringstream ss; - ss << "String table overflow"; ss << "\n File: " << esm.getName(); ss << "\n Record: " << esm.getContext().recName.toString(); ss << "\n Subrecord: " << "SCVR"; ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); Log(Debug::Verbose) << ss.str(); + // Get rid of empty strings in the list. + mVarNames.resize(i+1); break; } - } } From af36b652aacadd55dac91eb3256b2c8f9eb6bd2a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 4 Jan 2020 10:45:11 +0400 Subject: [PATCH 08/25] Simplify datetime formatting --- apps/openmw/mwgui/savegamedialog.cpp | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index e9fb2a964..98449d507 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -400,17 +400,7 @@ namespace MWGui struct tm* timeinfo; timeinfo = localtime(&time); - // Use system/environment locale settings for datetime formatting - char* oldLctime = setlocale(LC_TIME, nullptr); - setlocale(LC_TIME, ""); - - const int size=1024; - char buffer[size]; - if (std::strftime(buffer, size, "%x %X", timeinfo) > 0) - text << buffer << "\n"; - - // reset - setlocale(LC_TIME, oldLctime); + text << std::put_time(timeinfo, "%d.%m.%Y %T") << "\n"; text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n"; From 29c6a8975a8c47c6efb717080688fe23b8cb5e8a Mon Sep 17 00:00:00 2001 From: capostrophic Date: Sun, 5 Jan 2020 15:09:02 +0300 Subject: [PATCH 09/25] Don't combine sneak idle with scripted/wander idles (bug #4284) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/character.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b34443809..148135c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Bug #4262: Rain settings are hardcoded Bug #4270: Closing doors while they are obstructed desyncs closing sfx Bug #4276: Resizing character window differs from vanilla + Bug #4284: ForceSneak behaviour is inconsistent if the target has AiWander package Bug #4329: Removed birthsign abilities are restored after reloading the save Bug #4341: Error message about missing GDB is too vague Bug #4383: Bow model obscures crosshair when arrow is drawn diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index fa3fe4bd2..0c13c2740 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2249,7 +2249,7 @@ void CharacterController::update(float duration, bool animationOnly) if(movestate != CharState_None && !isTurning()) clearAnimQueue(); - if(mAnimQueue.empty() || inwater || sneak) + if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) { if (inwater) idlestate = CharState_IdleSwim; From 9b28420875073486ff55bc3f5bc1b78a559d3176 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Sun, 5 Jan 2020 23:30:22 +0300 Subject: [PATCH 10/25] [Regression] Make creatures autoequip shields properly again --- apps/openmw/mwworld/inventorystore.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 094d1c8ec..1c8062011 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -504,14 +504,10 @@ void MWWorld::InventoryStore::autoEquipShield(const MWWorld::Ptr& actor, TSlots& continue; if (iter->getClass().canBeEquipped(*iter, actor).first != 1) continue; - if (iter->getClass().getItemHealth(*iter) <= 0) - continue; std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); - if (shieldSlots.first.empty()) - continue; int slot = shieldSlots.first[0]; - const ContainerStoreIterator& shield = mSlots[slot]; + const ContainerStoreIterator& shield = slots_[slot]; if (shield != end() && shield.getType() == Type_Armor && shield->get()->mBase->mData.mType == ESM::Armor::Shield) { From a42396254bd7f97bab8c64852630860a8b845c48 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 6 Jan 2020 11:27:11 +0400 Subject: [PATCH 11/25] Use ISO format to print datetime --- apps/openmw/mwgui/savegamedialog.cpp | 2 +- apps/openmw/mwscript/miscextensions.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 98449d507..f85bfc8d3 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -400,7 +400,7 @@ namespace MWGui struct tm* timeinfo; timeinfo = localtime(&time); - text << std::put_time(timeinfo, "%d.%m.%Y %T") << "\n"; + text << std::put_time(timeinfo, "%Y.%m.%d %T") << "\n"; text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n"; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index ada1af59e..8be3945a9 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1228,7 +1228,7 @@ namespace MWScript msg << "Report time: "; std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - msg << std::put_time(std::gmtime(¤tTime), "%d.%m.%Y %T UTC") << std::endl; + msg << std::put_time(std::gmtime(¤tTime), "%Y.%m.%d %T UTC") << std::endl; msg << "Content file: "; From 2d87d287ba033b9ac6976d01f26d5585f31f4753 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Mon, 6 Jan 2020 15:09:32 +0300 Subject: [PATCH 12/25] Handle out-of-range actors' travel packages (#5212) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 10 +++++++++- apps/openmw/mwmechanics/aisequence.cpp | 8 ++++++-- apps/openmw/mwmechanics/aisequence.hpp | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6a78761..19337301b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,7 @@ Bug #5209: Spellcasting ignores race height Bug #5210: AiActivate allows actors to open dialogue and inventory windows Bug #5211: Screen fades in if the first loaded save is in interior cell + Bug #5212: AiTravel does not work for actors outside of AI processing range Bug #5213: SameFaction script function is broken Bug #5218: Crash when disabling ToggleBorders Bug #5220: GetLOS crashes when actor isn't loaded diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 35a48710d..cbe058a00 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1695,6 +1695,11 @@ namespace MWMechanics } } } + else if (aiActive && iter->first != player && isConscious(iter->first)) + { + CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); + } if(iter->first.getClass().isNpc()) { @@ -1722,7 +1727,10 @@ namespace MWMechanics { const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); bool isPlayer = iter->first == player; - bool inRange = isPlayer || dist <= mActorsProcessingRange; + CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + int packageId = stats.getAiSequence().getTypeId(); + bool travelling = (packageId == AiPackage::TypeIdTravel) || (packageId == AiPackage::TypeIdInternalTravel); + bool inRange = isPlayer || dist <= mActorsProcessingRange || travelling; int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) if (isPlayer) activeFlag = 2; diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index e02240352..0c675bfc1 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -198,7 +198,7 @@ bool isActualAiPackage(int packageTypeId) packageTypeId <= AiPackage::TypeIdActivate); } -void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration) +void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) { if(actor != getPlayer()) { @@ -214,7 +214,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac if (isActualAiPackage(packageTypeId)) mLastAiPackage = packageTypeId; // if active package is combat one, choose nearest target - if (packageTypeId == AiPackage::TypeIdCombat) + if (!outOfRange && packageTypeId == AiPackage::TypeIdCombat) { std::list::iterator itActualCombat; @@ -272,6 +272,10 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac try { + if (outOfRange && packageTypeId != AiPackage::TypeIdTravel + && packageTypeId != AiPackage::TypeIdInternalTravel) + return; + if (package->execute (actor, characterController, mAiState, duration)) { // Put repeating noncombat AI packages on the end of the stack so they can be used again diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 5df440407..7f07d5aae 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -110,7 +110,7 @@ namespace MWMechanics void stopPursuit(); /// Execute current package, switching if needed. - void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration); + void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange=false); /// Simulate the passing of time using the currently active AI package void fastForward(const MWWorld::Ptr &actor); From cb1a8ec518f72fe8f353ae5417391a0beb612a0f Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 9 Jan 2020 15:57:37 +0400 Subject: [PATCH 13/25] Do not link the Boost threads library --- components/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 412717aaa..33a458b87 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -217,7 +217,6 @@ add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) target_link_libraries(components ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} - ${Boost_THREAD_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_IOSTREAMS_LIBRARY} ${OSG_LIBRARIES} From 1cdd33b434a9c23717a37fea6865b412bc2bcc13 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 9 Jan 2020 15:57:05 +0400 Subject: [PATCH 14/25] Implement additional stringops to avoid Boost functions --- apps/openmw/mwgui/settingswindow.cpp | 8 ++-- apps/openmw/mwrender/renderingmanager.cpp | 6 +-- components/esmterrain/storage.cpp | 9 ++-- components/misc/stringops.hpp | 52 +++++++++++++++++++++++ components/settings/parser.cpp | 15 +++---- components/shader/shadermanager.cpp | 6 +-- components/shader/shadervisitor.cpp | 9 ++-- 7 files changed, 76 insertions(+), 29 deletions(-) diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index dc89e6134..aec3396a1 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -7,8 +7,6 @@ #include #include -#include - #include #include @@ -45,10 +43,10 @@ namespace void parseResolution (int &x, int &y, const std::string& str) { std::vector split; - boost::algorithm::split (split, str, boost::is_any_of("@(x")); + Misc::StringUtils::split (str, split, "@(x"); assert (split.size() >= 2); - boost::trim(split[0]); - boost::trim(split[1]); + Misc::StringUtils::trim(split[0]); + Misc::StringUtils::trim(split[1]); x = MyGUI::utility::parseInt (split[0]); y = MyGUI::utility::parseInt (split[1]); } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 8f7b339b8..175f141ba 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -22,6 +22,8 @@ #include +#include + #include #include #include @@ -47,8 +49,6 @@ #include -#include - #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwgui/loadingscreen.hpp" @@ -767,7 +767,7 @@ namespace MWRender int screenshotMapping = 0; std::vector settingArgs; - boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); + Misc::StringUtils::split(settingStr, settingArgs); if (settingArgs.size() > 0) { diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index 021ae32e9..fc93706a3 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -7,10 +7,9 @@ #include #include -#include - #include #include +#include #include namespace ESMTerrain @@ -564,7 +563,7 @@ namespace ESMTerrain if (mAutoUseNormalMaps) { std::string texture_ = texture; - boost::replace_last(texture_, ".", mNormalHeightMapPattern + "."); + Misc::StringUtils::replaceLast(texture_, ".", mNormalHeightMapPattern + "."); if (mVFS->exists(texture_)) { info.mNormalMap = texture_; @@ -573,7 +572,7 @@ namespace ESMTerrain else { texture_ = texture; - boost::replace_last(texture_, ".", mNormalMapPattern + "."); + Misc::StringUtils::replaceLast(texture_, ".", mNormalMapPattern + "."); if (mVFS->exists(texture_)) info.mNormalMap = texture_; } @@ -582,7 +581,7 @@ namespace ESMTerrain if (mAutoUseSpecularMaps) { std::string texture_ = texture; - boost::replace_last(texture_, ".", mSpecularMapPattern + "."); + Misc::StringUtils::replaceLast(texture_, ".", mSpecularMapPattern + "."); if (mVFS->exists(texture_)) { info.mDiffuseMap = texture_; diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index ba27d4775..192299c7c 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -245,6 +245,58 @@ public: { return format(fmt.c_str(), args ...); } + + static inline void trim(std::string &s) + { + // left trim + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) + { + return !std::isspace(ch); + })); + + // right trim + s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) + { + return !std::isspace(ch); + }).base(), s.end()); + } + + template + static inline void split(const std::string& str, Container& cont, const std::string& delims = " ") + { + std::size_t current, previous = 0; + current = str.find_first_of(delims); + while (current != std::string::npos) + { + cont.push_back(str.substr(previous, current - previous)); + previous = current + 1; + current = str.find_first_of(delims, previous); + } + cont.push_back(str.substr(previous, current - previous)); + } + + // TODO: use the std::string_view once we will use the C++17. + // It should allow us to avoid data copying while we still will support both string and literal arguments. + + static inline void replaceAll(std::string& data, std::string toSearch, std::string replaceStr) + { + size_t pos = data.find(toSearch); + + while( pos != std::string::npos) + { + data.replace(pos, toSearch.size(), replaceStr); + pos = data.find(toSearch, pos + replaceStr.size()); + } + } + + static inline void replaceLast(std::string& str, std::string substr, std::string with) + { + size_t pos = str.rfind(substr); + if (pos == std::string::npos) + return; + + str.replace(pos, substr.size(), with); + } }; } diff --git a/components/settings/parser.cpp b/components/settings/parser.cpp index 3767bb15d..e256da0d6 100644 --- a/components/settings/parser.cpp +++ b/components/settings/parser.cpp @@ -3,10 +3,9 @@ #include #include +#include #include -#include -#include void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings) { @@ -36,7 +35,7 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat fail("unterminated category"); currentCategory = line.substr(i+1, end - (i+1)); - boost::algorithm::trim(currentCategory); + Misc::StringUtils::trim(currentCategory); i = end+1; } @@ -51,11 +50,11 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat fail("unterminated setting name"); std::string setting = line.substr(i, (settingEnd-i)); - boost::algorithm::trim(setting); + Misc::StringUtils::trim(setting); size_t valueBegin = settingEnd+1; std::string value = line.substr(valueBegin); - boost::algorithm::trim(value); + Misc::StringUtils::trim(value); if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); @@ -142,7 +141,7 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con // Update the current category. currentCategory = line.substr(i+1, end - (i+1)); - boost::algorithm::trim(currentCategory); + Misc::StringUtils::trim(currentCategory); // Write the (new) current category to the file. ostream << "[" << currentCategory << "]" << std::endl; @@ -176,12 +175,12 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con continue; } std::string setting = line.substr(i, (settingEnd-i)); - boost::algorithm::trim(setting); + Misc::StringUtils::trim(setting); // Get the existing value so we can see if we've changed it. size_t valueBegin = settingEnd+1; std::string value = line.substr(valueBegin); - boost::algorithm::trim(value); + Misc::StringUtils::trim(value); // Construct the setting map key to determine whether the setting has already been // written to the file. diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 97fc79562..0a7345b97 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -8,9 +8,9 @@ #include #include -#include #include +#include namespace Shader { @@ -60,7 +60,7 @@ namespace Shader bool parseIncludes(boost::filesystem::path shaderPath, std::string& source) { - boost::replace_all(source, "\r\n", "\n"); + Misc::StringUtils::replaceAll(source, "\r\n", "\n"); std::set includedFiles; size_t foundPos = 0; @@ -165,7 +165,7 @@ namespace Shader std::string list = source.substr(listStart, listEnd - listStart); std::vector listElements; if (list != "") - boost::split(listElements, list, boost::is_any_of(",")); + Misc::StringUtils::split (list, listElements, ","); size_t contentStart = source.find_first_not_of("\n\r", listEnd); size_t contentEnd = source.find("$endforeach", contentStart); diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index e40cc255b..ae2da0947 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -6,9 +6,8 @@ #include -#include - #include +#include #include #include #include @@ -145,7 +144,7 @@ namespace Shader osg::ref_ptr image; bool normalHeight = false; std::string normalHeightMap = normalMapFileName; - boost::replace_last(normalHeightMap, ".", mNormalHeightMapPattern + "."); + Misc::StringUtils::replaceLast(normalHeightMap, ".", mNormalHeightMapPattern + "."); if (mImageManager.getVFS()->exists(normalHeightMap)) { image = mImageManager.getImage(normalHeightMap); @@ -153,7 +152,7 @@ namespace Shader } else { - boost::replace_last(normalMapFileName, ".", mNormalMapPattern + "."); + Misc::StringUtils::replaceLast(normalMapFileName, ".", mNormalMapPattern + "."); if (mImageManager.getVFS()->exists(normalMapFileName)) { image = mImageManager.getImage(normalMapFileName); @@ -184,7 +183,7 @@ namespace Shader if (mAutoUseSpecularMaps && diffuseMap != nullptr && specularMap == nullptr && diffuseMap->getImage(0)) { std::string specularMapFileName = diffuseMap->getImage(0)->getFileName(); - boost::replace_last(specularMapFileName, ".", mSpecularMapPattern + "."); + Misc::StringUtils::replaceLast(specularMapFileName, ".", mSpecularMapPattern + "."); if (mImageManager.getVFS()->exists(specularMapFileName)) { osg::ref_ptr image (mImageManager.getImage(specularMapFileName)); From a250a405b4388136c4910dd8622f439c471307fb Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 9 Jan 2020 20:42:06 +0400 Subject: [PATCH 15/25] An attempt to fix the MSVC2017 build --- components/misc/stringops.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 192299c7c..aa2ae105e 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -1,6 +1,7 @@ #ifndef MISC_STRINGOPS_H #define MISC_STRINGOPS_H +#include #include #include From 42cc27194b89d9ff2737d96d76c1e3a56590073e Mon Sep 17 00:00:00 2001 From: capostrophic Date: Sat, 4 Jan 2020 16:07:59 +0300 Subject: [PATCH 16/25] Fix reported spellcasting discrepancies Make ExplodeSpell behavior closer to Cast behavior (#5242) Nullify on-self absorb spells in a different way (#5241) Allow casting permanent spells through Cast/ExplodeSpell --- CHANGELOG.md | 2 ++ apps/openmw/mwmechanics/spellcasting.cpp | 19 +++++++++++++-- apps/openmw/mwscript/miscextensions.cpp | 31 ++++++++++++++++++------ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea093d8f..d4649c46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,8 @@ Bug #5226: Reputation should be capped Bug #5229: Crash if mesh controller node has no data node Bug #5239: OpenMW-CS does not support non-ASCII characters in path names + Bug #5241: On-self absorb spells cannot be detected + Bug #5242: ExplodeSpell behavior differs from Cast behavior Feature #1774: Handle AvoidNode Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 4598cd75b..5d3b83a62 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -447,9 +447,9 @@ namespace MWMechanics if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer)) continue; - // caster needs to be an actor that's not the target for linked effects (e.g. Absorb) + // caster needs to be an actor for linked effects (e.g. Absorb) if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked - && (caster.isEmpty() || !caster.getClass().isActor() || caster == target)) + && (caster.isEmpty() || !caster.getClass().isActor())) continue; // If player is healing someone, show the target's HP bar @@ -552,6 +552,20 @@ namespace MWMechanics effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; effect.mMagnitude = magnitude; + // Avoid applying absorb effects if the caster is the target + // We still need the spell to be added + if (caster == target) + { + for (int i=0; i<5; ++i) + { + if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i) + { + effect.mMagnitude = 0; + break; + } + } + } + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); if (hasDuration && effectIt->mDuration == 0) { @@ -614,6 +628,7 @@ namespace MWMechanics else caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true, absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId()); + break; } } } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 8be3945a9..4f84cfe47 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1109,12 +1109,6 @@ namespace MWScript return; } - if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) - { - runtime.getContext().report("spellcasting failed: you can only cast spells and powers."); - return; - } - if (ptr == MWMechanics::getPlayer()) { MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); @@ -1152,9 +1146,32 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spell = runtime.getStringLiteral (runtime[0].mInteger); + std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); + if (!spell) + { + runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\""); + return; + } + + if (ptr == MWMechanics::getPlayer()) + { + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + store.setSelectedEnchantItem(store.end()); + MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, ptr))); + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + return; + } + + if (ptr.getClass().isActor()) + { + MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + return; + } + MWMechanics::CastSpell cast(ptr, ptr, false, true); cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; From af2ea477d5efac748ed5c8dd3a943b46a1aaf160 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Thu, 9 Jan 2020 13:39:12 +0300 Subject: [PATCH 17/25] Don't use loops to detect absorb effects --- apps/openmw/mwmechanics/spellcasting.cpp | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 5d3b83a62..0b7c27b86 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -554,16 +554,11 @@ namespace MWMechanics // Avoid applying absorb effects if the caster is the target // We still need the spell to be added - if (caster == target) + if (caster == target + && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute + && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) { - for (int i=0; i<5; ++i) - { - if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i) - { - effect.mMagnitude = 0; - break; - } - } + effect.mMagnitude = 0; } bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); @@ -614,22 +609,19 @@ namespace MWMechanics // magnitude, since we're transferring stats from the target to the caster if (!caster.isEmpty() && caster != target && caster.getClass().isActor()) { - for (int i=0; i<5; ++i) + if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && + effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) { - if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i) - { - std::vector absorbEffects; - ActiveSpells::ActiveEffect effect_ = effect; - effect_.mMagnitude *= -1; - absorbEffects.push_back(effect_); - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true, - absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId()); - else - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true, - absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId()); - break; - } + std::vector absorbEffects; + ActiveSpells::ActiveEffect effect_ = effect; + effect_.mMagnitude *= -1; + absorbEffects.push_back(effect_); + if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true, + absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId()); + else + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true, + absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId()); } } } From 1251b32f0fc9144da23d6ad12ee1100c27c04a88 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Thu, 9 Jan 2020 13:55:14 +0300 Subject: [PATCH 18/25] Slightly reduce code duplication --- apps/openmw/mwscript/miscextensions.cpp | 11 ++--------- apps/openmw/mwworld/player.cpp | 12 ++++++++++++ apps/openmw/mwworld/player.hpp | 2 ++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 4f84cfe47..ccf285844 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1111,10 +1111,7 @@ namespace MWScript if (ptr == MWMechanics::getPlayer()) { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, ptr))); - MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); return; } @@ -1122,7 +1119,6 @@ namespace MWScript { MWMechanics::AiCast castPackage(targetId, spellId, true); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); - return; } @@ -1158,10 +1154,7 @@ namespace MWScript if (ptr == MWMechanics::getPlayer()) { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, ptr))); - MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); return; } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 8e047677b..8c5f52655 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -11,6 +11,7 @@ #include #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,6 +20,7 @@ #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "class.hpp" #include "ptr.hpp" @@ -492,4 +494,14 @@ namespace MWWorld { mPreviousItems.erase(boundItemId); } + + void Player::setSelectedSpell(const std::string& spellId) + { + Ptr player = getPlayer(); + InventoryStore& store = player.getClass().getInventoryStore(player); + store.setSelectedEnchantItem(store.end()); + int castChance = int(MWMechanics::getSpellSuccessChance(spellId, player)); + MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, castChance); + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + } } diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index 96ed20adf..1e4b0ffdf 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -135,6 +135,8 @@ namespace MWWorld void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId); std::string getPreviousItem(const std::string& boundItemId); void erasePreviousItem(const std::string& boundItemId); + + void setSelectedSpell(const std::string& spellId); }; } #endif From fc0f813dcb57a18186bbc8d386f80b881d6fc770 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Thu, 9 Jan 2020 14:11:53 +0300 Subject: [PATCH 19/25] Add and use 'always active' AI package flag Update documentation --- apps/openmw/mwmechanics/actors.cpp | 11 ++++++++--- apps/openmw/mwmechanics/aipackage.hpp | 3 +++ apps/openmw/mwmechanics/aisequence.cpp | 9 ++++----- apps/openmw/mwmechanics/aitravel.hpp | 2 ++ docs/source/reference/modding/settings/game.rst | 10 +++++----- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index cbe058a00..b49dcd469 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1728,9 +1728,14 @@ namespace MWMechanics const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); bool isPlayer = iter->first == player; CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - int packageId = stats.getAiSequence().getTypeId(); - bool travelling = (packageId == AiPackage::TypeIdTravel) || (packageId == AiPackage::TypeIdInternalTravel); - bool inRange = isPlayer || dist <= mActorsProcessingRange || travelling; + // Actors with active AI should be able to move. + bool alwaysActive = false; + if (!isPlayer && isConscious(iter->first) && !stats.isParalyzed()) + { + MWMechanics::AiSequence& seq = stats.getAiSequence(); + alwaysActive = !seq.isEmpty() && seq.getActivePackage()->alwaysActive(); + } + bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive; int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) if (isPlayer) activeFlag = 2; diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 0e628388d..b9b3baf64 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -104,6 +104,9 @@ namespace MWMechanics virtual osg::Vec3f getDestination() { return osg::Vec3f(0, 0, 0); } + // Return true if any loaded actor with this AI package must be active. + virtual bool alwaysActive() const { return false; } + /// Reset pathfinding state void reset(); diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 0c675bfc1..5760069e7 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -209,12 +209,15 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac } MWMechanics::AiPackage* package = mPackages.front(); + if (!package->alwaysActive() && outOfRange) + return; + int packageTypeId = package->getTypeId(); // workaround ai packages not being handled as in the vanilla engine if (isActualAiPackage(packageTypeId)) mLastAiPackage = packageTypeId; // if active package is combat one, choose nearest target - if (!outOfRange && packageTypeId == AiPackage::TypeIdCombat) + if (packageTypeId == AiPackage::TypeIdCombat) { std::list::iterator itActualCombat; @@ -272,10 +275,6 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac try { - if (outOfRange && packageTypeId != AiPackage::TypeIdTravel - && packageTypeId != AiPackage::TypeIdInternalTravel) - return; - if (package->execute (actor, characterController, mAiState, duration)) { // Put repeating noncombat AI packages on the end of the stack so they can be used again diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index ea69b5d74..a333c83fd 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -34,6 +34,8 @@ namespace MWMechanics virtual bool useVariableSpeed() const { return true;} + virtual bool alwaysActive() const { return true; } + virtual osg::Vec3f getDestination() { return osg::Vec3f(mX, mY, mZ); } private: diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 162cb0e2f..9a3e9c84d 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -103,13 +103,13 @@ actors processing range :Range: 3584 to 7168 :Default: 7168 -This setting allows to specify a distance from player in game units, in which OpenMW updates actor's state. +This setting specifies the actor state update distance from the player in game units. Actor state update includes AI, animations, and physics processing. -Actors near that border start softly fade out instead of just appearing/disapperaing. -It is not recommended to change this value from default if you use mods with -long-range AiTravel packages (e.g. patrols, caravans and travellers). +Actors close to this distance softly fade in and out instead of appearing or disappearing abruptly. +Keep in mind that actors running Travel AI packages are always active to avoid +issues in mods with long-range AiTravel packages (for example, patrols, caravans and travellers). -This setting can be controlled in game with the "Actors processing range slider" in the Prefs panel of the Options menu. +This setting can be controlled in game with the "Actors Processing Range" slider in the Prefs panel of the Options menu. classic reflected absorb spells behavior ---------------------------------------- From 2b8e976c6abe59aa8137c809821b8ceb0135cb7a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 10 Jan 2020 14:07:53 +0400 Subject: [PATCH 20/25] Adjust settings menu --- files/mygui/openmw_settings_window.layout | 38 +++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index ce1fb15ed..f61af5a10 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -213,7 +213,7 @@ - + @@ -231,10 +231,10 @@ - + - + @@ -244,15 +244,15 @@ - + - + - + @@ -306,9 +306,9 @@ - + - + @@ -320,18 +320,18 @@ - + - + - + - + @@ -341,11 +341,11 @@ - + - + @@ -373,10 +373,10 @@ - + - + @@ -388,7 +388,7 @@ - + @@ -400,11 +400,11 @@ - + - + From 4d4663e0ad878fc8ec32ef60b411cdae14ba50d0 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 10 Jan 2020 14:29:04 +0400 Subject: [PATCH 21/25] Make 'Toggle HUD' hotkey configurable --- apps/openmw/mwinput/inputmanagerimp.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index c395333d6..540efdf89 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1601,6 +1601,8 @@ namespace MWInput return "Zoom In"; else if (action == A_ZoomOut) return "Zoom Out"; + else if (action == A_ToggleHUD) + return "Toggle HUD"; descriptions[A_Use] = "sUse"; descriptions[A_Activate] = "sActivate"; @@ -1779,6 +1781,7 @@ namespace MWInput ret.push_back(A_Console); ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); + ret.push_back(A_ToggleHUD); ret.push_back(A_Screenshot); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); @@ -1812,6 +1815,7 @@ namespace MWInput ret.push_back(A_Rest); ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); + ret.push_back(A_ToggleHUD); ret.push_back(A_Screenshot); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); @@ -1852,7 +1856,7 @@ namespace MWInput } // Disallow binding reserved keys - if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10 || key == SDL_SCANCODE_F11) + if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10) return; #ifndef __APPLE__ From bbe5adb8601dd56ca76fb3f69ee1a9590b928861 Mon Sep 17 00:00:00 2001 From: capostrophic Date: Fri, 10 Jan 2020 13:00:57 +0300 Subject: [PATCH 22/25] Use shield body part model for creatures (bug #5250) --- CHANGELOG.md | 1 + apps/openmw/mwrender/actoranimation.cpp | 26 ++++++++++++++++++++++ apps/openmw/mwrender/creatureanimation.cpp | 24 +++++++++++++++++++- apps/openmw/mwrender/npcanimation.cpp | 17 +++++++++----- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3178c609b..3ca82279e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,6 +191,7 @@ Bug #5239: OpenMW-CS does not support non-ASCII characters in path names Bug #5241: On-self absorb spells cannot be detected Bug #5242: ExplodeSpell behavior differs from Cast behavior + Bug #5250: Creatures display shield ground mesh instead of shield body part Feature #1774: Handle AvoidNode Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 71811db68..fcffe220b 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -27,6 +27,7 @@ #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" @@ -87,6 +88,31 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const { std::string mesh = shield.getClass().getModel(shield); + const ESM::Armor *armor = shield.get()->mBase; + const std::vector& bodyparts = armor->mParts.mParts; + if (!bodyparts.empty()) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::Store &partStore = store.get(); + + // Try to get shield model from bodyparts first, with ground model as fallback + for (const auto& part : bodyparts) + { + // Assume all creatures use the male mesh. + if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + continue; + const ESM::BodyPart *bodypart = partStore.search(part.mMale); + if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + { + mesh = "meshes\\" + bodypart->mModel; + break; + } + } + } + + if (mesh.empty()) + return mesh; + std::string holsteredName = mesh; holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); if(mResourceSystem->getVFS()->exists(holsteredName)) diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index baa695cda..8e7f30687 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -13,11 +13,13 @@ #include +#include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" namespace MWRender { @@ -114,6 +116,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) MWWorld::ConstPtr item = *it; std::string bonename; + std::string itemModel = item.getClass().getModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { if(item.getTypeName() == typeid(ESM::Weapon).name()) @@ -132,11 +135,30 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) bonename = "Weapon Bone"; } else + { bonename = "Shield Bone"; + if (item.getTypeName() == typeid(ESM::Armor).name()) + { + // Shield body part model should be used if possible. + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + for (const auto& part : item.get()->mBase->mParts.mParts) + { + // Assume all creatures use the male mesh. + if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + continue; + const ESM::BodyPart *bodypart = store.get().search(part.mMale); + if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + { + itemModel = "meshes\\" + bodypart->mModel; + break; + } + } + } + } try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(item.getClass().getModel(item)); + osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(itemModel); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index e9f62e25c..b63962b3e 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -518,14 +518,14 @@ std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const { std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; - std::vector bodyparts = armor->mParts.mParts; + const std::vector& bodyparts = armor->mParts.mParts; if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); - // For NPCs try to get shield model from bodyparts first, with ground model as fallback - for (auto & part : bodyparts) + // Try to get shield model from bodyparts first, with ground model as fallback + for (const auto& part : bodyparts) { if (part.mPart != ESM::PRT_Shield) continue; @@ -538,16 +538,21 @@ std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const if (!bodypartName.empty()) { - const ESM::BodyPart *bodypart = 0; - bodypart = partStore.search(bodypartName); + const ESM::BodyPart *bodypart = partStore.search(bodypartName); if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return ""; + return std::string(); else if (!bodypart->mModel.empty()) + { mesh = "meshes\\" + bodypart->mModel; + break; + } } } } + if (mesh.empty()) + return std::string(); + std::string holsteredName = mesh; holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); if(mResourceSystem->getVFS()->exists(holsteredName)) From a1c0a480ce5cbaec75c04b1dfa93a354222f2866 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 10 Jan 2020 15:50:25 +0400 Subject: [PATCH 23/25] Add missing changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca82279e..50501a54d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,6 +135,7 @@ Bug #5063: Shape named "Tri Shadow" in creature mesh is visible if it isn't hidden Bug #5067: Ranged attacks on unaware opponents ("critical hits") differ from the vanilla engine Bug #5069: Blocking creatures' attacks doesn't degrade shields + Bug #5073: NPCs open doors in front of them even if they don't have to Bug #5074: Paralyzed actors greet the player Bug #5075: Enchanting cast style can be changed if there's no object Bug #5078: DisablePlayerLooking is broken From a384104f73c467844769391b16001ae654a37078 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 11 Jan 2020 21:47:08 +0400 Subject: [PATCH 24/25] Rework greeting timeouts (bug #5249) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50501a54d..5674cd3c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,6 +192,7 @@ Bug #5239: OpenMW-CS does not support non-ASCII characters in path names Bug #5241: On-self absorb spells cannot be detected Bug #5242: ExplodeSpell behavior differs from Cast behavior + Bug #5249: Wandering NPCs start walking too soon after they hello Bug #5250: Creatures display shield ground mesh instead of shield body part Feature #1774: Handle AvoidNode Feature #2229: Improve pathfinding AI diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index b49dcd469..e2bf08d82 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -142,8 +142,9 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float namespace MWMechanics { - static const int GREETING_SHOULD_START = 4; //how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 10; + static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement static const float DECELERATE_DISTANCE = 512.f; class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor @@ -519,9 +520,10 @@ namespace MWMechanics { greetingTimer++; - turnActorToFacePlayer(actor, dir); + if (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor)) + turnActorToFacePlayer(actor, dir); - if (greetingTimer >= GREETING_SHOULD_END) + if (greetingTimer >= GREETING_COOLDOWN) { greetingState = Greet_Done; greetingTimer = 0; From 7659370992ad7f118dda7e70ae1db57a5d4baa71 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Mon, 13 Jan 2020 02:24:48 +0300 Subject: [PATCH 25/25] Editor: Add a reset to defaults button to keybindings (feature #4068) --- CHANGELOG.md | 1 + apps/opencs/view/prefs/keybindingpage.cpp | 17 +++++++++++++++++ apps/opencs/view/prefs/keybindingpage.hpp | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50501a54d..54b4fa0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -204,6 +204,7 @@ Feature #3980: In-game option to disable controller Feature #3999: Shift + Double Click should maximize/restore menu size Feature #4001: Toggle sneak controller shortcut + Feature #4068: OpenMW-CS: Add a button to reset key bindings to defaults Feature #4129: Beta Comment to File Feature #4209: Editor: Faction rank sub-table Feature #4255: Handle broken RepairedOnMe script function diff --git a/apps/opencs/view/prefs/keybindingpage.cpp b/apps/opencs/view/prefs/keybindingpage.cpp index 143665f4a..eed5c0eb8 100644 --- a/apps/opencs/view/prefs/keybindingpage.cpp +++ b/apps/opencs/view/prefs/keybindingpage.cpp @@ -4,11 +4,13 @@ #include #include +#include #include #include #include "../../model/prefs/setting.hpp" #include "../../model/prefs/category.hpp" +#include "../../model/prefs/state.hpp" namespace CSVPrefs { @@ -29,8 +31,18 @@ namespace CSVPrefs mPageSelector = new QComboBox(); connect(mPageSelector, SIGNAL(currentIndexChanged(int)), mStackedLayout, SLOT(setCurrentIndex(int))); + QFrame* lineSeparator = new QFrame(topWidget); + lineSeparator->setFrameShape(QFrame::HLine); + lineSeparator->setFrameShadow(QFrame::Sunken); + + // Reset key bindings button + QPushButton* resetButton = new QPushButton ("Reset to Defaults", topWidget); + connect(resetButton, SIGNAL(clicked()), this, SLOT(resetKeyBindings())); + topLayout->addWidget(mPageSelector); topLayout->addWidget(stackedWidget); + topLayout->addWidget(lineSeparator); + topLayout->addWidget(resetButton); topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); // Add each option @@ -85,4 +97,9 @@ namespace CSVPrefs } } } + + void KeyBindingPage::resetKeyBindings() + { + CSMPrefs::State::get().resetCategory("Key Bindings"); + } } diff --git a/apps/opencs/view/prefs/keybindingpage.hpp b/apps/opencs/view/prefs/keybindingpage.hpp index 8a0cb2952..05c4b22db 100644 --- a/apps/opencs/view/prefs/keybindingpage.hpp +++ b/apps/opencs/view/prefs/keybindingpage.hpp @@ -29,6 +29,10 @@ namespace CSVPrefs QStackedLayout* mStackedLayout; QGridLayout* mPageLayout; QComboBox* mPageSelector; + + private slots: + + void resetKeyBindings(); }; }