From f6226e4859c0a089156a4d4c16e7335ff94c3b79 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 8 Aug 2013 15:55:23 +0200 Subject: [PATCH 001/194] added Creator class for filters --- apps/opencs/CMakeLists.txt | 4 ++ apps/opencs/model/filter/filter.hpp | 12 ++--- apps/opencs/view/filter/filtercreator.cpp | 65 +++++++++++++++++++++++ apps/opencs/view/filter/filtercreator.hpp | 41 ++++++++++++++ apps/opencs/view/world/subviews.cpp | 8 ++- 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 apps/opencs/view/filter/filtercreator.cpp create mode 100644 apps/opencs/view/filter/filtercreator.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index f7b7daee4..9f33c862a 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -111,6 +111,10 @@ opencs_hdrs_noqt (model/filter filter ) +opencs_units (view/filter + filtercreator + ) + set (OPENCS_US ) diff --git a/apps/opencs/model/filter/filter.hpp b/apps/opencs/model/filter/filter.hpp index bddc9cc2d..7630ad410 100644 --- a/apps/opencs/model/filter/filter.hpp +++ b/apps/opencs/model/filter/filter.hpp @@ -11,15 +11,15 @@ namespace CSMFilter /// \brief Wrapper for Filter record struct Filter : public ESM::Filter { - enum scope + enum Scope { - Global = 0, - Local = 1, - Session = 2, - Content = 3 + Scope_Global = 0, // per user + Scope_Project = 1, // per project + Scope_Session = 2, // exists only for one editing session; not saved + Scope_Content = 3 // embedded in the edited content file }; - scope mScope; + Scope mScope; }; } diff --git a/apps/opencs/view/filter/filtercreator.cpp b/apps/opencs/view/filter/filtercreator.cpp new file mode 100644 index 000000000..476eacbe1 --- /dev/null +++ b/apps/opencs/view/filter/filtercreator.cpp @@ -0,0 +1,65 @@ + +#include "filtercreator.hpp" + +#include +#include + +#include "../../model/filter/filter.hpp" + +std::string CSVFilter::FilterCreator::getNamespace() const +{ + switch (mScope->currentIndex()) + { + case CSMFilter::Filter::Scope_Global: return "global::"; + case CSMFilter::Filter::Scope_Project: return "project::"; + case CSMFilter::Filter::Scope_Session: return "session::"; + } + + return ""; +} + +void CSVFilter::FilterCreator::update() +{ + mNamespace->setText (QString::fromUtf8 (getNamespace().c_str())); + GenericCreator::update(); +} + +std::string CSVFilter::FilterCreator::getId() const +{ + return getNamespace() + GenericCreator::getId(); +} + +CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id) +: GenericCreator (data, undoStack, id) +{ + mNamespace = new QLabel ("::", this); + insertAtBeginning (mNamespace, false); + + mScope = new QComboBox (this); + + mScope->addItem ("Global"); + mScope->addItem ("Project"); + mScope->addItem ("Session"); + /// \ŧodo re-enable for OpenMW 1.1 + // mScope->addItem ("Content"); + + connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (setScope (int))); + + insertAtBeginning (mScope, false); + + QLabel *label = new QLabel ("Scope", this); + insertAtBeginning (label, false); + + mScope->setCurrentIndex (2); +} + +void CSVFilter::FilterCreator::reset() +{ + GenericCreator::reset(); +} + +void CSVFilter::FilterCreator::setScope (int index) +{ + update(); +} diff --git a/apps/opencs/view/filter/filtercreator.hpp b/apps/opencs/view/filter/filtercreator.hpp new file mode 100644 index 000000000..82d38d22c --- /dev/null +++ b/apps/opencs/view/filter/filtercreator.hpp @@ -0,0 +1,41 @@ +#ifndef CSV_FILTER_FILTERCREATOR_H +#define CSV_FILTER_FILTERCREATOR_H + +class QComboBox; +class QLabel; + +#include "../world/genericcreator.hpp" + +namespace CSVFilter +{ + class FilterCreator : public CSVWorld::GenericCreator + { + Q_OBJECT + + QComboBox *mScope; + QLabel *mNamespace; + + private: + + std::string getNamespace() const; + + protected: + + void update(); + + virtual std::string getId() const; + + public: + + FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id); + + virtual void reset(); + + private slots: + + void setScope (int index); + }; +} + +#endif diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 2ca711a59..d22e07d89 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -3,6 +3,8 @@ #include "../doc/subviewfactoryimp.hpp" +#include "../filter/filtercreator.hpp" + #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" @@ -33,7 +35,6 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, - CSMWorld::UniversalId::Type_Filters, CSMWorld::UniversalId::Type_None // end marker }; @@ -56,4 +57,9 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) // Other stuff (combined record tables) manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + + manager.add (CSMWorld::UniversalId::Type_Filters, + new CSVDoc::SubViewFactoryWithCreator >); + } \ No newline at end of file From a95715b61dbd52237fc21454e1f639bf2b5d3e47 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 9 Aug 2013 13:45:50 +0200 Subject: [PATCH 002/194] added description field to filter record --- apps/opencs/model/world/data.cpp | 1 + components/esm/filter.cpp | 3 +++ components/esm/filter.hpp | 2 ++ 3 files changed, 6 insertions(+) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 9c5e13562..4a5dcb38f 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -150,6 +150,7 @@ CSMWorld::Data::Data() : mRefs (mCells) mFilters.addColumn (new StringIdColumn); mFilters.addColumn (new RecordStateColumn); + mFilters.addColumn (new DescriptionColumn); addModel (new IdTable (&mGlobals), UniversalId::Type_Globals, UniversalId::Type_Global); addModel (new IdTable (&mGmsts), UniversalId::Type_Gmsts, UniversalId::Type_Gmst); diff --git a/components/esm/filter.cpp b/components/esm/filter.cpp index 205332f6b..7d4851a5f 100644 --- a/components/esm/filter.cpp +++ b/components/esm/filter.cpp @@ -7,14 +7,17 @@ void ESM::Filter::load (ESMReader& esm) { mFilter = esm.getHNString ("FILT"); + mDescription = esm.getHNString ("DESC"); } void ESM::Filter::save (ESMWriter& esm) { esm.writeHNCString ("FILT", mFilter); + esm.writeHNCString ("DESC", mDescription); } void ESM::Filter::blank() { mFilter.clear(); + mDescription.clear(); } diff --git a/components/esm/filter.hpp b/components/esm/filter.hpp index 2dde92fb0..0fd564361 100644 --- a/components/esm/filter.hpp +++ b/components/esm/filter.hpp @@ -12,6 +12,8 @@ namespace ESM { std::string mId; + std::string mDescription; + std::string mFilter; void load (ESMReader& esm); From f5b06d5d520573dd612e663131a28f7b6cc3eed5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 9 Aug 2013 14:49:05 +0200 Subject: [PATCH 003/194] basic filter tree structure --- apps/opencs/CMakeLists.txt | 4 ++ apps/opencs/model/filter/binarynode.cpp | 58 ++++++++++++++++++++++++ apps/opencs/model/filter/binarynode.hpp | 42 +++++++++++++++++ apps/opencs/model/filter/booleannode.cpp | 17 +++++++ apps/opencs/model/filter/booleannode.hpp | 31 +++++++++++++ apps/opencs/model/filter/leafnode.cpp | 22 +++++++++ apps/opencs/model/filter/leafnode.hpp | 29 ++++++++++++ apps/opencs/model/filter/node.cpp | 6 +++ apps/opencs/model/filter/node.hpp | 58 ++++++++++++++++++++++++ apps/opencs/model/filter/unarynode.cpp | 34 ++++++++++++++ apps/opencs/model/filter/unarynode.hpp | 37 +++++++++++++++ 11 files changed, 338 insertions(+) create mode 100644 apps/opencs/model/filter/binarynode.cpp create mode 100644 apps/opencs/model/filter/binarynode.hpp create mode 100644 apps/opencs/model/filter/booleannode.cpp create mode 100644 apps/opencs/model/filter/booleannode.hpp create mode 100644 apps/opencs/model/filter/leafnode.cpp create mode 100644 apps/opencs/model/filter/leafnode.hpp create mode 100644 apps/opencs/model/filter/node.cpp create mode 100644 apps/opencs/model/filter/node.hpp create mode 100644 apps/opencs/model/filter/unarynode.cpp create mode 100644 apps/opencs/model/filter/unarynode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 9f33c862a..8e0024ad9 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -107,6 +107,10 @@ opencs_units_noqt (model/settings settingsitem ) +opencs_units_noqt (model/filter + node unarynode binarynode leafnode booleannode + ) + opencs_hdrs_noqt (model/filter filter ) diff --git a/apps/opencs/model/filter/binarynode.cpp b/apps/opencs/model/filter/binarynode.cpp new file mode 100644 index 000000000..0e86930db --- /dev/null +++ b/apps/opencs/model/filter/binarynode.cpp @@ -0,0 +1,58 @@ + +#include "binarynode.hpp" + +CSMFilter::BinaryNode::BinaryNode (std::auto_ptr left, std::auto_ptr right) +: mLeft (left), mRight (right) +{} + +const CSMFilter::Node& CSMFilter::BinaryNode::getLeft() const +{ + return *mLeft; +} + +CSMFilter::Node& CSMFilter::BinaryNode::getLeft() +{ + return *mLeft; +} + +const CSMFilter::Node& CSMFilter::BinaryNode::getRight() const +{ + return *mRight; +} + +CSMFilter::Node& CSMFilter::BinaryNode::getRight() +{ + return *mRight; +} + +std::vector CSMFilter::BinaryNode::getReferencedFilters() const +{ + std::vector left = mLeft->getReferencedFilters(); + + std::vector right = mRight->getReferencedFilters(); + + left.insert (left.end(), right.begin(), right.end()); + + return left; +} + +std::vector CSMFilter::BinaryNode::getReferencedColumns() const +{ + std::vector left = mLeft->getReferencedColumns(); + + std::vector right = mRight->getReferencedColumns(); + + left.insert (left.end(), right.begin(), right.end()); + + return left; +} + +bool CSMFilter::BinaryNode::isSimple() const +{ + return false; +} + +bool CSMFilter::BinaryNode::hasUserValue() const +{ + return mLeft->hasUserValue() || mRight->hasUserValue(); +} diff --git a/apps/opencs/model/filter/binarynode.hpp b/apps/opencs/model/filter/binarynode.hpp new file mode 100644 index 000000000..18cdad2a9 --- /dev/null +++ b/apps/opencs/model/filter/binarynode.hpp @@ -0,0 +1,42 @@ +#ifndef CSM_FILTER_BINARYNODE_H +#define CSM_FILTER_BINARYNODE_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class BinaryNode : public Node + { + std::auto_ptr mLeft; + std::auto_ptr mRight; + + public: + + BinaryNode (std::auto_ptr left, std::auto_ptr right); + + const Node& getLeft() const; + + Node& getLeft(); + + const Node& getRight() const; + + Node& getRight(); + + virtual std::vector getReferencedFilters() const; + ///< Return a list of filters that are used by this node (and must be passed as + /// otherFilters when calling test). + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual bool isSimple() const; + ///< \return Can this filter be displayed in simple mode. + + virtual bool hasUserValue() const; + }; +} + +#endif diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp new file mode 100644 index 000000000..cea130d24 --- /dev/null +++ b/apps/opencs/model/filter/booleannode.cpp @@ -0,0 +1,17 @@ + +#include "booleannode.hpp" + +CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true) {} + +bool CSMFilter::BooleanNode::test (const CSMWorld::IdTable& table, int row, + const std::map& otherFilters, + const std::map& columns, + const std::string& userValue) const +{ + return mTrue; +} + +std::string CSMFilter::BooleanNode::toString (bool numericColumns) const +{ + return mTrue ? "true" : "false"; +} \ No newline at end of file diff --git a/apps/opencs/model/filter/booleannode.hpp b/apps/opencs/model/filter/booleannode.hpp new file mode 100644 index 000000000..3578c7f3f --- /dev/null +++ b/apps/opencs/model/filter/booleannode.hpp @@ -0,0 +1,31 @@ +#ifndef CSM_FILTER_BOOLEANNODE_H +#define CSM_FILTER_BOOLEANNODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class BooleanNode : public LeafNode + { + bool mTrue; + + public: + + BooleanNode (bool true_); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& otherFilters, + const std::map& columns, + const std::string& userValue) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/filter/leafnode.cpp b/apps/opencs/model/filter/leafnode.cpp new file mode 100644 index 000000000..5c6d4b042 --- /dev/null +++ b/apps/opencs/model/filter/leafnode.cpp @@ -0,0 +1,22 @@ + +#include "leafnode.hpp" + +std::vector CSMFilter::LeafNode::getReferencedFilters() const +{ + return std::vector(); +} + +std::vector CSMFilter::LeafNode::getReferencedColumns() const +{ + return std::vector(); +} + +bool CSMFilter::LeafNode::isSimple() const +{ + return true; +} + +bool CSMFilter::LeafNode::hasUserValue() const +{ + return false; +} \ No newline at end of file diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp new file mode 100644 index 000000000..663083ff9 --- /dev/null +++ b/apps/opencs/model/filter/leafnode.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_FILTER_UNARIYNODE_H +#define CSM_FILTER_UNARIYNODE_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class LeafNode : public Node + { + public: + + virtual std::vector getReferencedFilters() const; + ///< Return a list of filters that are used by this node (and must be passed as + /// otherFilters when calling test). + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual bool isSimple() const; + ///< \return Can this filter be displayed in simple mode. + + virtual bool hasUserValue() const; + }; +} + +#endif diff --git a/apps/opencs/model/filter/node.cpp b/apps/opencs/model/filter/node.cpp new file mode 100644 index 000000000..276861cdc --- /dev/null +++ b/apps/opencs/model/filter/node.cpp @@ -0,0 +1,6 @@ + +#include "node.hpp" + +CSMFilter::Node::Node() {} + +CSMFilter::Node::~Node() {} \ No newline at end of file diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp new file mode 100644 index 000000000..09bc3cd42 --- /dev/null +++ b/apps/opencs/model/filter/node.hpp @@ -0,0 +1,58 @@ +#ifndef CSM_FILTER_NODE_H +#define CSM_FILTER_NODE_H + +#include +#include +#include + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSMFilter +{ + /// \brief Root class for the filter node hierarchy + /// + /// \note When the function documentation for this class mentions "this node", this should be + /// interpreted as "the node and all its children". + class Node + { + // not implemented + Node (const Node&); + Node& operator= (const Node&); + + public: + + Node(); + + virtual ~Node(); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& otherFilters, + const std::map& columns, + const std::string& userValue) const = 0; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedFilters() const = 0; + ///< Return a list of filters that are used by this node (and must be passed as + /// otherFilters when calling test). + + virtual std::vector getReferencedColumns() const = 0; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual bool isSimple() const = 0; + ///< \return Can this filter be displayed in simple mode. + + virtual bool hasUserValue() const = 0; + + virtual std::string toString (bool numericColumns) const = 0; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp new file mode 100644 index 000000000..f9cc3fdc8 --- /dev/null +++ b/apps/opencs/model/filter/unarynode.cpp @@ -0,0 +1,34 @@ + +#include "unarynode.hpp" + +CSMFilter::UnaryNode::UnaryNode (std::auto_ptr child) : mChild (child) {} + +const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const +{ + return *mChild; +} + +CSMFilter::Node& CSMFilter::UnaryNode::getChild() +{ + return *mChild; +} + +std::vector CSMFilter::UnaryNode::getReferencedFilters() const +{ + return mChild->getReferencedFilters(); +} + +std::vector CSMFilter::UnaryNode::getReferencedColumns() const +{ + return mChild->getReferencedColumns(); +} + +bool CSMFilter::UnaryNode::isSimple() const +{ + return false; +} + +bool CSMFilter::UnaryNode::hasUserValue() const +{ + return mChild->hasUserValue(); +} diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp new file mode 100644 index 000000000..6ecad1192 --- /dev/null +++ b/apps/opencs/model/filter/unarynode.hpp @@ -0,0 +1,37 @@ +#ifndef CSM_FILTER_UNARIYNODE_H +#define CSM_FILTER_UNARIYNODE_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class UnaryNode : public Node + { + std::auto_ptr mChild; + + public: + + UnaryNode (std::auto_ptr child); + + const Node& getChild() const; + + Node& getChild(); + + virtual std::vector getReferencedFilters() const; + ///< Return a list of filters that are used by this node (and must be passed as + /// otherFilters when calling test). + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual bool isSimple() const; + ///< \return Can this filter be displayed in simple mode. + + virtual bool hasUserValue() const; + }; +} + +#endif From 789a66eaa7e0894e458c39bae8afa993fc392c0e Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 11 Aug 2013 20:39:21 +0200 Subject: [PATCH 004/194] added filter box and basic record filter UI (not functional yet; only supporting on-the-fly filters for now) --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/filter/filterbox.cpp | 18 ++++++++++++++++++ apps/opencs/view/filter/filterbox.hpp | 19 +++++++++++++++++++ apps/opencs/view/filter/recordfilterbox.cpp | 20 ++++++++++++++++++++ apps/opencs/view/filter/recordfilterbox.hpp | 21 +++++++++++++++++++++ apps/opencs/view/world/tablesubview.cpp | 4 ++++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 apps/opencs/view/filter/filterbox.cpp create mode 100644 apps/opencs/view/filter/filterbox.hpp create mode 100644 apps/opencs/view/filter/recordfilterbox.cpp create mode 100644 apps/opencs/view/filter/recordfilterbox.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 8e0024ad9..d363eeedc 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -116,7 +116,7 @@ opencs_hdrs_noqt (model/filter ) opencs_units (view/filter - filtercreator + filtercreator filterbox recordfilterbox ) set (OPENCS_US diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp new file mode 100644 index 000000000..f3f17706b --- /dev/null +++ b/apps/opencs/view/filter/filterbox.cpp @@ -0,0 +1,18 @@ + +#include "filterbox.hpp" + +#include + +#include "recordfilterbox.hpp" + +CSVFilter::FilterBox::FilterBox (QWidget *parent) +: QWidget (parent) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + layout->setContentsMargins (0, 0, 0, 0); + + layout->addWidget (new RecordFilterBox (this)); + + setLayout (layout); +} \ No newline at end of file diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp new file mode 100644 index 000000000..969a43cd7 --- /dev/null +++ b/apps/opencs/view/filter/filterbox.hpp @@ -0,0 +1,19 @@ +#ifndef CSV_FILTER_FILTERBOX_H +#define CSV_FILTER_FILTERBOX_H + +#include + +namespace CSVFilter +{ + class FilterBox : public QWidget + { + Q_OBJECT + + public: + + FilterBox (QWidget *parent = 0); + }; + +} + +#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp new file mode 100644 index 000000000..b0c195664 --- /dev/null +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -0,0 +1,20 @@ + +#include "recordfilterbox.hpp" + +#include +#include +#include + +CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) +: QWidget (parent) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + layout->setContentsMargins (0, 0, 0, 0); + + layout->addWidget (new QLabel ("Record Filter", this)); + + layout->addWidget (new QLineEdit (this)); + + setLayout (layout); +} \ No newline at end of file diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp new file mode 100644 index 000000000..3a411f808 --- /dev/null +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -0,0 +1,21 @@ +#ifndef CSV_FILTER_RECORDFILTERBOX_H +#define CSV_FILTER_RECORDFILTERBOX_H + +#include + +#include + +namespace CSVFilter +{ + class RecordFilterBox : public QWidget + { + Q_OBJECT + + public: + + RecordFilterBox (QWidget *parent = 0); + }; + +} + +#endif \ No newline at end of file diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index af3d186e8..df95940c9 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -5,6 +5,8 @@ #include "../../model/doc/document.hpp" +#include "../filter/filterbox.hpp" + #include "table.hpp" #include "tablebottombox.hpp" #include "creator.hpp" @@ -23,6 +25,8 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D layout->insertWidget (0, mTable = new Table (id, document.getData(), document.getUndoStack(), mBottom->canCreateAndDelete()), 2); + layout->insertWidget (0, new CSVFilter::FilterBox (this)); + QWidget *widget = new QWidget; widget->setLayout (layout); From 528e047fd53d89b3c28a6074c60f272ec954545a Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 16 Aug 2013 13:57:54 +0200 Subject: [PATCH 005/194] repalced std::auto_ptr with boost::shared_ptr in filter nodes --- apps/opencs/model/filter/binarynode.cpp | 2 +- apps/opencs/model/filter/binarynode.hpp | 8 ++++---- apps/opencs/model/filter/unarynode.cpp | 2 +- apps/opencs/model/filter/unarynode.hpp | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/opencs/model/filter/binarynode.cpp b/apps/opencs/model/filter/binarynode.cpp index 0e86930db..c0c7fde37 100644 --- a/apps/opencs/model/filter/binarynode.cpp +++ b/apps/opencs/model/filter/binarynode.cpp @@ -1,7 +1,7 @@ #include "binarynode.hpp" -CSMFilter::BinaryNode::BinaryNode (std::auto_ptr left, std::auto_ptr right) +CSMFilter::BinaryNode::BinaryNode (boost::shared_ptr left, boost::shared_ptr right) : mLeft (left), mRight (right) {} diff --git a/apps/opencs/model/filter/binarynode.hpp b/apps/opencs/model/filter/binarynode.hpp index 18cdad2a9..c2b4041d2 100644 --- a/apps/opencs/model/filter/binarynode.hpp +++ b/apps/opencs/model/filter/binarynode.hpp @@ -1,7 +1,7 @@ #ifndef CSM_FILTER_BINARYNODE_H #define CSM_FILTER_BINARYNODE_H -#include +#include #include "node.hpp" @@ -9,12 +9,12 @@ namespace CSMFilter { class BinaryNode : public Node { - std::auto_ptr mLeft; - std::auto_ptr mRight; + boost::shared_ptr mLeft; + boost::shared_ptr mRight; public: - BinaryNode (std::auto_ptr left, std::auto_ptr right); + BinaryNode (boost::shared_ptr left, boost::shared_ptr right); const Node& getLeft() const; diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp index f9cc3fdc8..490d95376 100644 --- a/apps/opencs/model/filter/unarynode.cpp +++ b/apps/opencs/model/filter/unarynode.cpp @@ -1,7 +1,7 @@ #include "unarynode.hpp" -CSMFilter::UnaryNode::UnaryNode (std::auto_ptr child) : mChild (child) {} +CSMFilter::UnaryNode::UnaryNode (boost::shared_ptr child) : mChild (child) {} const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const { diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp index 6ecad1192..4e1fb0cc2 100644 --- a/apps/opencs/model/filter/unarynode.hpp +++ b/apps/opencs/model/filter/unarynode.hpp @@ -1,7 +1,7 @@ #ifndef CSM_FILTER_UNARIYNODE_H #define CSM_FILTER_UNARIYNODE_H -#include +#include #include "node.hpp" @@ -9,11 +9,11 @@ namespace CSMFilter { class UnaryNode : public Node { - std::auto_ptr mChild; + boost::shared_ptr mChild; public: - UnaryNode (std::auto_ptr child); + UnaryNode (boost::shared_ptr child); const Node& getChild() const; From 236dc9fc43ceec38bed853e8326f828de2dbf87c Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 16 Aug 2013 14:18:31 +0200 Subject: [PATCH 006/194] replaced binary filter node with a n-ary node --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/filter/binarynode.cpp | 58 ------------------ apps/opencs/model/filter/narynode.cpp | 61 +++++++++++++++++++ .../filter/{binarynode.hpp => narynode.hpp} | 21 +++---- 4 files changed, 71 insertions(+), 71 deletions(-) delete mode 100644 apps/opencs/model/filter/binarynode.cpp create mode 100644 apps/opencs/model/filter/narynode.cpp rename apps/opencs/model/filter/{binarynode.hpp => narynode.hpp} (64%) diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index d363eeedc..8a1949e51 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode binarynode leafnode booleannode + node unarynode narynode leafnode booleannode ) opencs_hdrs_noqt (model/filter diff --git a/apps/opencs/model/filter/binarynode.cpp b/apps/opencs/model/filter/binarynode.cpp deleted file mode 100644 index c0c7fde37..000000000 --- a/apps/opencs/model/filter/binarynode.cpp +++ /dev/null @@ -1,58 +0,0 @@ - -#include "binarynode.hpp" - -CSMFilter::BinaryNode::BinaryNode (boost::shared_ptr left, boost::shared_ptr right) -: mLeft (left), mRight (right) -{} - -const CSMFilter::Node& CSMFilter::BinaryNode::getLeft() const -{ - return *mLeft; -} - -CSMFilter::Node& CSMFilter::BinaryNode::getLeft() -{ - return *mLeft; -} - -const CSMFilter::Node& CSMFilter::BinaryNode::getRight() const -{ - return *mRight; -} - -CSMFilter::Node& CSMFilter::BinaryNode::getRight() -{ - return *mRight; -} - -std::vector CSMFilter::BinaryNode::getReferencedFilters() const -{ - std::vector left = mLeft->getReferencedFilters(); - - std::vector right = mRight->getReferencedFilters(); - - left.insert (left.end(), right.begin(), right.end()); - - return left; -} - -std::vector CSMFilter::BinaryNode::getReferencedColumns() const -{ - std::vector left = mLeft->getReferencedColumns(); - - std::vector right = mRight->getReferencedColumns(); - - left.insert (left.end(), right.begin(), right.end()); - - return left; -} - -bool CSMFilter::BinaryNode::isSimple() const -{ - return false; -} - -bool CSMFilter::BinaryNode::hasUserValue() const -{ - return mLeft->hasUserValue() || mRight->hasUserValue(); -} diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp new file mode 100644 index 000000000..0756f6707 --- /dev/null +++ b/apps/opencs/model/filter/narynode.cpp @@ -0,0 +1,61 @@ + +#include "narynode.hpp" + +CSMFilter::NAryNode::NAryNode (const std::vector >& nodes) +: mNodes (nodes) +{} + +int CSMFilter::NAryNode::getSize() const +{ + return mNodes.size(); +} + +const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const +{ + return *mNodes.at (index); +} + +std::vector CSMFilter::NAryNode::getReferencedFilters() const +{ + std::vector filters; + + for (std::vector >::const_iterator iter (mNodes.begin()); + iter!=mNodes.end(); ++iter) + { + std::vector filters2 = (*iter)->getReferencedFilters(); + + filters.insert (filters.end(), filters2.begin(), filters2.end()); + } + + return filters; +} + +std::vector CSMFilter::NAryNode::getReferencedColumns() const +{ + std::vector columns; + + for (std::vector >::const_iterator iter (mNodes.begin()); + iter!=mNodes.end(); ++iter) + { + std::vector columns2 = (*iter)->getReferencedColumns(); + + columns.insert (columns.end(), columns2.begin(), columns2.end()); + } + + return columns; +} + +bool CSMFilter::NAryNode::isSimple() const +{ + return false; +} + +bool CSMFilter::NAryNode::hasUserValue() const +{ + for (std::vector >::const_iterator iter (mNodes.begin()); + iter!=mNodes.end(); ++iter) + if ((*iter)->hasUserValue()) + return true; + + return false; +} diff --git a/apps/opencs/model/filter/binarynode.hpp b/apps/opencs/model/filter/narynode.hpp similarity index 64% rename from apps/opencs/model/filter/binarynode.hpp rename to apps/opencs/model/filter/narynode.hpp index c2b4041d2..65e12d1ed 100644 --- a/apps/opencs/model/filter/binarynode.hpp +++ b/apps/opencs/model/filter/narynode.hpp @@ -1,5 +1,7 @@ -#ifndef CSM_FILTER_BINARYNODE_H -#define CSM_FILTER_BINARYNODE_H +#ifndef CSM_FILTER_NARYNODE_H +#define CSM_FILTER_NARYNODE_H + +#include #include @@ -7,22 +9,17 @@ namespace CSMFilter { - class BinaryNode : public Node + class NAryNode : public Node { - boost::shared_ptr mLeft; - boost::shared_ptr mRight; + std::vector > mNodes; public: - BinaryNode (boost::shared_ptr left, boost::shared_ptr right); - - const Node& getLeft() const; - - Node& getLeft(); + NAryNode (const std::vector >& nodes); - const Node& getRight() const; + int getSize() const; - Node& getRight(); + const Node& operator[] (int index) const; virtual std::vector getReferencedFilters() const; ///< Return a list of filters that are used by this node (and must be passed as From 40fa11577c7a3e43ac013d231e3b1a6c0ddfb605 Mon Sep 17 00:00:00 2001 From: eroen Date: Fri, 16 Aug 2013 22:32:16 +0200 Subject: [PATCH 007/194] Re-introduce lost functionality The branch merged in 5a863589b4 removed fine-grained configure-time control over install paths. This is necessary to accomodate various linux distros' policies, eg. Gentoo wants games installed in /usr/games, but with resource files in /usr/share/games. DOCDIR and MANDIR appear to be unused, and were not re-introduced. --- CMakeLists.txt | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 575fecd0c..85860609b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -334,12 +334,18 @@ IF(NOT WIN32 AND NOT APPLE) IF (DPKG_PROGRAM) ## Debian specific SET(CMAKE_INSTALL_PREFIX "/usr") + SET(DATAROOTDIR "share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(DATADIR "share/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "share/pixmaps" CACHE PATH "Set icon dir") SET(SYSCONFDIR "../etc/openmw" CACHE PATH "Set config dir") ELSE () ## Non debian specific SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") - SET(LICDIR "${CMAKE_INSTALL_PREFIX}/share/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") + SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") # Install binaries INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) @@ -367,11 +373,11 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF (DPKG_PROGRAM) # Install icon and desktop file - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "share/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "share/pixmaps/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "share/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "share/pixmaps/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files @@ -383,8 +389,8 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF(BUILD_OPENCS) # Install resources - INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "share/games/openmw/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") - INSTALL(DIRECTORY DESTINATION "share/games/openmw/data/" COMPONENT "Resources") + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") + INSTALL(DIRECTORY DESTINATION "${DATADIR}/data/" COMPONENT "Resources") IF (DPKG_PROGRAM) ## Debian Specific From 1a88e6d859be92dc689bc9e2ebc3dac9286e8586 Mon Sep 17 00:00:00 2001 From: eroen Date: Fri, 16 Aug 2013 22:32:16 +0200 Subject: [PATCH 008/194] cleanup - drop trailing slashes from paths for consistency - sort entries that got unsorted --- CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85860609b..dd58ed416 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -340,12 +340,12 @@ IF(NOT WIN32 AND NOT APPLE) SET(SYSCONFDIR "../etc/openmw" CACHE PATH "Set config dir") ELSE () ## Non debian specific - SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") # Install binaries INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) @@ -373,11 +373,11 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF (DPKG_PROGRAM) # Install icon and desktop file - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files @@ -389,8 +389,8 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF(BUILD_OPENCS) # Install resources - INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") - INSTALL(DIRECTORY DESTINATION "${DATADIR}/data/" COMPONENT "Resources") + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") + INSTALL(DIRECTORY DESTINATION "${DATADIR}/data" COMPONENT "Resources") IF (DPKG_PROGRAM) ## Debian Specific From d82f54c771a1664b3a6907ec92230be098a42290 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 01:11:57 -0700 Subject: [PATCH 009/194] Improve actor movement collision handling --- apps/openmw/mwworld/physicssystem.cpp | 127 ++++++++++++-------------- 1 file changed, 56 insertions(+), 71 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 371783bc5..e8450e1d2 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -31,17 +31,22 @@ namespace MWWorld static const float sMaxSlope = 60.0f; static const float sStepSize = 30.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. - static const int sMaxIterations = 50; + static const int sMaxIterations = 4; class MovementSolver { private: - static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, const Ogre::Vector3 &velocity, float remainingTime, + static float getSlope(const Ogre::Vector3 &normal) + { + return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); + } + + static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, + const Ogre::Vector3 &velocity, float &remainingTime, const Ogre::Vector3 &halfExtents, bool isInterior, OEngine::Physic::PhysicEngine *engine) { - traceResults trace; // no initialization needed - + traceResults trace; newtrace(&trace, orient, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); if(trace.fraction == 0.0f) @@ -51,45 +56,34 @@ namespace MWWorld halfExtents, isInterior, engine); if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) return false; + float movefrac = trace.fraction; newtrace(&trace, orient, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); if(getSlope(trace.planenormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. position = trace.endpos; + remainingTime *= (1.0f-movefrac); return true; } return false; } - static void clipVelocity(Ogre::Vector3& inout, const Ogre::Vector3& normal, float overbounce=1.0f) - { - //Math stuff. Basically just project the velocity vector onto the plane represented by the normal. - //More specifically, it projects velocity onto the normal, takes that result, multiplies it by overbounce and then subtracts it from velocity. - float backoff = inout.dotProduct(normal); - if(backoff < 0.0f) - backoff *= overbounce; - else - backoff /= overbounce; - - inout -= normal*backoff; - } - static void projectVelocity(Ogre::Vector3& velocity, const Ogre::Vector3& direction) + ///Project a vector u on another vector v + static inline Ogre::Vector3 project(const Ogre::Vector3 u, const Ogre::Vector3 &v) { - Ogre::Vector3 normalizedDirection(direction); - normalizedDirection.normalise(); - - // no divide by normalizedDirection.length necessary because it's normalized - velocity = normalizedDirection * velocity.dotProduct(normalizedDirection); + return v * u.dotProduct(v); } - static float getSlope(const Ogre::Vector3 &normal) + ///Helper for computing the character sliding + static inline Ogre::Vector3 slide(Ogre::Vector3 direction, const Ogre::Vector3 &planeNormal) { - return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); + return direction - project(direction, planeNormal); } + public: static Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, OEngine::Physic::PhysicEngine *engine) { @@ -104,7 +98,6 @@ namespace MWWorld return position; bool wasCollisionMode = physicActor->getCollisionMode(); - physicActor->enableCollisions(false); Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); @@ -124,10 +117,9 @@ namespace MWWorld if (wasCollisionMode) physicActor->enableCollisions(true); - if (hit) - return newPosition+Ogre::Vector3(0,0,4); - else + if(!hit) return position; + return newPosition+Ogre::Vector3(0,0,4); } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, @@ -147,14 +139,13 @@ namespace MWWorld movement; } - traceResults trace; //no initialization needed - bool onground = false; - float remainingTime = time; bool isInterior = !ptr.getCell()->isExterior(); Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); - bool wasCollisionMode = physicActor->getCollisionMode(); physicActor->enableCollisions(false); - Ogre::Quaternion orient = Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[2]), Ogre::Vector3::UNIT_Z); + + traceResults trace; + bool onground = false; + const Ogre::Quaternion orient; // Don't rotate actor collision boxes Ogre::Vector3 velocity; if(!gravity) { @@ -175,53 +166,46 @@ namespace MWWorld velocity.z += physicActor->getVerticalForce(); } - Ogre::Vector3 clippedVelocity(velocity); if(onground) { - // if we're on the ground, force velocity to track it - clippedVelocity.z = velocity.z = std::max(0.0f, velocity.z); - clipVelocity(clippedVelocity, trace.planenormal); + // if we're on the ground, don't try to fall + velocity.z = std::max(0.0f, velocity.z); } const Ogre::Vector3 up(0.0f, 0.0f, 1.0f); Ogre::Vector3 newPosition = position; - int iterations = 0; - do { + float remainingTime = time; + for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) + { // trace to where character would go if there were no obstructions - newtrace(&trace, orient, newPosition, newPosition+clippedVelocity*remainingTime, halfExtents, isInterior, engine); - newPosition = trace.endpos; - remainingTime = remainingTime * (1.0f-trace.fraction); + newtrace(&trace, orient, newPosition, newPosition+velocity*remainingTime, halfExtents, isInterior, engine); // check for obstructions - if(trace.fraction < 1.0f) + if(trace.fraction >= 1.0f) { - //std::cout<<"angle: "< 0.0f); + //std::cout<<"angle: "<setOnGround(onground); - physicActor->setVerticalForce(!onground ? clippedVelocity.z - time*627.2f : 0.0f); - if (wasCollisionMode) - physicActor->enableCollisions(true); + physicActor->setVerticalForce(!onground ? velocity.z - time*627.2f : 0.0f); + physicActor->enableCollisions(true); + return newPosition; } }; From 65ce3c6ba5512885d1e0b252f539a4e66d084d82 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 02:51:19 -0700 Subject: [PATCH 010/194] Use a better method to do actor physics traces --- apps/openmw/mwworld/physicssystem.cpp | 35 ++++++------ libs/openengine/bullet/physic.hpp | 48 +++++++++------- libs/openengine/bullet/trace.cpp | 82 +++++++++++++++++++++++++-- libs/openengine/bullet/trace.h | 4 ++ 4 files changed, 124 insertions(+), 45 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index e8450e1d2..ea013bc7a 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -41,24 +41,21 @@ namespace MWWorld return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); } - static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, + static bool stepMove(btCollisionObject *colobj, Ogre::Vector3 &position, const Ogre::Vector3 &velocity, float &remainingTime, - const Ogre::Vector3 &halfExtents, bool isInterior, OEngine::Physic::PhysicEngine *engine) { traceResults trace; - newtrace(&trace, orient, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), - halfExtents, isInterior, engine); + actortrace(&trace, colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); if(trace.fraction == 0.0f) return false; - newtrace(&trace, orient, trace.endpos, trace.endpos + velocity*remainingTime, - halfExtents, isInterior, engine); + actortrace(&trace, colobj, trace.endpos, trace.endpos + velocity*remainingTime, engine); if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) return false; float movefrac = trace.fraction; - newtrace(&trace, orient, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); + actortrace(&trace, colobj, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); if(getSlope(trace.planenormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. @@ -100,7 +97,7 @@ namespace MWWorld bool wasCollisionMode = physicActor->getCollisionMode(); physicActor->enableCollisions(false); - Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); + Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); halfExtents.z = 1; // we trace the feet only, so we use a very thin box Ogre::Vector3 newPosition = position; @@ -133,15 +130,15 @@ namespace MWWorld if(!physicActor || !physicActor->getCollisionMode()) { // FIXME: This works, but it's inconcsistent with how the rotations are applied elsewhere. Why? - return position + (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* - Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* - Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * + return position + (Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)* + Ogre::Quaternion(Ogre::Radian(-refpos.rot[1]), Ogre::Vector3::UNIT_Y)* + Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * movement; } - bool isInterior = !ptr.getCell()->isExterior(); - Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); - physicActor->enableCollisions(false); + btCollisionObject *colobj = physicActor->getCollisionBody(); + Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); + position.z += halfExtents.z; traceResults trace; bool onground = false; @@ -158,7 +155,7 @@ namespace MWWorld { if(!(movement.z > 0.0f)) { - newtrace(&trace, orient, position, position-Ogre::Vector3(0,0,4), halfExtents, isInterior, engine); + actortrace(&trace, colobj, position, position-Ogre::Vector3(0,0,4), engine); if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) onground = true; } @@ -178,7 +175,7 @@ namespace MWWorld for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) { // trace to where character would go if there were no obstructions - newtrace(&trace, orient, newPosition, newPosition+velocity*remainingTime, halfExtents, isInterior, engine); + actortrace(&trace, colobj, newPosition, newPosition+velocity*remainingTime, engine); // check for obstructions if(trace.fraction >= 1.0f) @@ -190,7 +187,7 @@ namespace MWWorld //std::cout<<"angle: "<setOnGround(onground); physicActor->setVerticalForce(!onground ? velocity.z - time*627.2f : 0.0f); - physicActor->enableCollisions(true); + newPosition.z -= halfExtents.z; return newPosition; } }; diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index 32af0da4e..9b1541ea2 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -66,6 +66,20 @@ namespace Physic std::string mName; }; + /** + *This class is just an extension of normal btRigidBody in order to add extra info. + *When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, + *so one never should use btRigidBody directly! + */ + class RigidBody: public btRigidBody + { + public: + RigidBody(btRigidBody::btRigidBodyConstructionInfo& CI,std::string name); + virtual ~RigidBody(); + std::string mName; + bool mPlaceable; + }; + /** * A physic actor uses a rigid body based on box shapes. * Pmove is used to move the physic actor around the dynamic world. @@ -129,47 +143,41 @@ namespace Physic bool getOnGround() const; + btCollisionObject *getCollisionBody() const + { + return mBody; + } + private: void disableCollisionBody(); void enableCollisionBody(); public: //HACK: in Visual Studio 2010 and presumably above, this structures alignment -// must be 16, but the built in operator new & delete don't properly -// perform this alignment. +// must be 16, but the built in operator new & delete don't properly +// perform this alignment. #if _MSC_VER >= 1600 - void * operator new (size_t Size) { return _aligned_malloc (Size, 16); } - void operator delete (void * Data) { _aligned_free (Data); } + void * operator new (size_t Size) { return _aligned_malloc (Size, 16); } + void operator delete (void * Data) { _aligned_free (Data); } #endif private: - OEngine::Physic::RigidBody* mBody; OEngine::Physic::RigidBody* mRaycastingBody; + Ogre::Vector3 mBoxScaledTranslation; - btQuaternion mBoxRotationInverse; Ogre::Quaternion mBoxRotation; + btQuaternion mBoxRotationInverse; + float verticalForce; bool onGround; bool collisionMode; + std::string mMesh; - PhysicEngine* mEngine; std::string mName; + PhysicEngine *mEngine; }; - /** - *This class is just an extension of normal btRigidBody in order to add extra info. - *When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, - *so one never should use btRigidBody directly! - */ - class RigidBody: public btRigidBody - { - public: - RigidBody(btRigidBody::btRigidBodyConstructionInfo& CI,std::string name); - virtual ~RigidBody(); - std::string mName; - bool mPlaceable; - }; struct HeightField { diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index d246417c7..08c42d9d0 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -8,12 +8,6 @@ #include "physic.hpp" -enum traceWorldType -{ - collisionWorldTrace = 1, - pickWorldTrace = 2, - bothWorldTrace = collisionWorldTrace | pickWorldTrace -}; void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine *enginePass) //Traceobj was a Aedra Object { @@ -46,3 +40,79 @@ void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre: results->fraction = 1.0f; } } + + +class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback +{ +public: + ClosestNotMeConvexResultCallback(btCollisionObject *me, const btVector3 &up, btScalar minSlopeDot) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), + mMe(me), mUp(up), mMinSlopeDot(minSlopeDot) + { + } + + virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) + { + if(convexResult.m_hitCollisionObject == mMe) + return btScalar( 1 ); + + btVector3 hitNormalWorld; + if(normalInWorldSpace) + hitNormalWorld = convexResult.m_hitNormalLocal; + else + { + ///need to transform normal into worldspace + hitNormalWorld = m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + } + + // NOTE : m_hitNormalLocal is not always vertical on the ground with a capsule or a box... + + btScalar dotUp = mUp.dot(hitNormalWorld); + if(dotUp < mMinSlopeDot) + return btScalar(1); + + return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); + } + +protected: + btCollisionObject *mMe; + const btVector3 mUp; + const btScalar mMinSlopeDot; +}; + +void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, OEngine::Physic::PhysicEngine *enginePass) +{ + const btVector3 btstart(start.x, start.y, start.z); + const btVector3 btend(end.x, end.y, end.z); + + const btTransform &trans = actor->getWorldTransform(); + btTransform from(trans); + btTransform to(trans); + from.setOrigin(btstart); + to.setOrigin(btend); + + ClosestNotMeConvexResultCallback newTraceCallback(actor, btstart-btend, btScalar(0.0)); + newTraceCallback.m_collisionFilterMask = OEngine::Physic::CollisionType_World | + OEngine::Physic::CollisionType_HeightMap | + OEngine::Physic::CollisionType_Actor; + + btCollisionShape *shape = actor->getCollisionShape(); + assert(shape->isConvex()); + enginePass->dynamicsWorld->convexSweepTest(static_cast(shape), + from, to, newTraceCallback); + + // Copy the hit data over to our trace results struct: + if(newTraceCallback.hasHit()) + { + const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; + results->fraction = newTraceCallback.m_closestHitFraction; + results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); + results->endpos = (end-start)*results->fraction + start; + } + else + { + results->endpos = end; + results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); + results->fraction = 1.0f; + } +} diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index cd2547f8c..79c6e72ef 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -13,6 +13,9 @@ namespace OEngine } +class btCollisionObject; + + struct traceResults { Ogre::Vector3 endpos; @@ -22,5 +25,6 @@ struct traceResults }; void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine* enginePass); +void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); #endif From 62c7b3698d47cdfb28486249458286d1a6bab318 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 03:55:04 -0700 Subject: [PATCH 011/194] Get rid of the old newtrace method --- apps/openmw/mwworld/physicssystem.cpp | 31 ++++++++---------------- libs/openengine/bullet/trace.cpp | 35 +-------------------------- libs/openengine/bullet/trace.h | 1 - 3 files changed, 11 insertions(+), 56 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index ea013bc7a..65cc806ea 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -87,36 +87,25 @@ namespace MWWorld const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); - bool hit=false; - bool isInterior = !ptr.getCell()->isExterior(); - OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle()); if (!physicActor) return position; - bool wasCollisionMode = physicActor->getCollisionMode(); - physicActor->enableCollisions(false); - - Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); - halfExtents.z = 1; // we trace the feet only, so we use a very thin box + const int maxHeight = 64.f; + Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); - Ogre::Vector3 newPosition = position; + traceResults trace; + actortrace(&trace, physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); + if(trace.fraction >= 1.0f) + return position; - traceResults trace; //no initialization needed + physicActor->setOnGround(getSlope(trace.planenormal) <= sMaxSlope); - int maxHeight = 200.f; - newtrace(&trace, Ogre::Quaternion::IDENTITY, newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), halfExtents, isInterior, engine); - if(trace.fraction < 1.0f) - hit = true; newPosition = trace.endpos; + newPosition.z -= physicActor->getHalfExtents().z; + newPosition.z += 4.0f; - physicActor->setOnGround(hit && getSlope(trace.planenormal) <= sMaxSlope); - if (wasCollisionMode) - physicActor->enableCollisions(true); - - if(!hit) - return position; - return newPosition+Ogre::Vector3(0,0,4); + return newPosition; } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index 08c42d9d0..744462a0c 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -9,39 +9,6 @@ #include "physic.hpp" -void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine *enginePass) //Traceobj was a Aedra Object -{ - const btVector3 btstart(start.x, start.y, start.z + BBHalfExtents.z); - const btVector3 btend(end.x, end.y, end.z + BBHalfExtents.z); - const btQuaternion btorient (orient.x, orient.y, orient.z, orient.w); - - const btBoxShape newshape(btVector3(BBHalfExtents.x, BBHalfExtents.y, BBHalfExtents.z)); - //const btCapsuleShapeZ newshape(BBHalfExtents.x, BBHalfExtents.z * 2 - BBHalfExtents.x * 2); - const btTransform from(btorient, btstart); - const btTransform to(btorient, btend); - - btCollisionWorld::ClosestConvexResultCallback newTraceCallback(btstart, btend); - newTraceCallback.m_collisionFilterMask = OEngine::Physic::CollisionType_World|OEngine::Physic::CollisionType_HeightMap|OEngine::Physic::CollisionType_Actor; - - enginePass->dynamicsWorld->convexSweepTest(&newshape, from, to, newTraceCallback); - - // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) - { - const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; - results->fraction = newTraceCallback.m_closestHitFraction; - results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); - results->endpos = (end-start)*results->fraction + start; - } - else - { - results->endpos = end; - results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); - results->fraction = 1.0f; - } -} - - class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: @@ -62,7 +29,7 @@ public: else { ///need to transform normal into worldspace - hitNormalWorld = m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; } // NOTE : m_hitNormalLocal is not always vertical on the ground with a capsule or a box... diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index 79c6e72ef..6353d6cfa 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -24,7 +24,6 @@ struct traceResults float fraction; }; -void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine* enginePass); void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); #endif From 8bcce0fb556b0f66d80fc345fb67735064c76461 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 04:42:35 -0700 Subject: [PATCH 012/194] Clean up the trace struct --- apps/openmw/mwworld/physicssystem.cpp | 55 ++++++++++++++------------- libs/openengine/bullet/trace.cpp | 23 +++++++---- libs/openengine/bullet/trace.h | 30 +++++++-------- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 65cc806ea..ee6514590 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -45,21 +45,22 @@ namespace MWWorld const Ogre::Vector3 &velocity, float &remainingTime, OEngine::Physic::PhysicEngine *engine) { - traceResults trace; - actortrace(&trace, colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(trace.fraction == 0.0f) + OEngine::Physic::ActorTracer tracer; + tracer.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(tracer.mFraction == 0.0f) return false; - actortrace(&trace, colobj, trace.endpos, trace.endpos + velocity*remainingTime, engine); - if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) + tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos + velocity*remainingTime, engine); + if(tracer.mFraction < std::numeric_limits::epsilon() || + (tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) > sMaxSlope)) return false; - float movefrac = trace.fraction; + float movefrac = tracer.mFraction; - actortrace(&trace, colobj, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(getSlope(trace.planenormal) <= sMaxSlope) + tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(getSlope(tracer.mPlaneNormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - position = trace.endpos; + position = tracer.mEndPos; remainingTime *= (1.0f-movefrac); return true; } @@ -94,16 +95,16 @@ namespace MWWorld const int maxHeight = 64.f; Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); - traceResults trace; - actortrace(&trace, physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); - if(trace.fraction >= 1.0f) + OEngine::Physic::ActorTracer tracer; + tracer.doTrace(physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); + if(tracer.mFraction >= 1.0f) return position; - physicActor->setOnGround(getSlope(trace.planenormal) <= sMaxSlope); + physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); - newPosition = trace.endpos; + newPosition = tracer.mEndPos; newPosition.z -= physicActor->getHalfExtents().z; - newPosition.z += 4.0f; + newPosition.z += 2.0f; return newPosition; } @@ -129,7 +130,7 @@ namespace MWWorld Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); position.z += halfExtents.z; - traceResults trace; + OEngine::Physic::ActorTracer tracer; bool onground = false; const Ogre::Quaternion orient; // Don't rotate actor collision boxes Ogre::Vector3 velocity; @@ -144,8 +145,8 @@ namespace MWWorld { if(!(movement.z > 0.0f)) { - actortrace(&trace, colobj, position, position-Ogre::Vector3(0,0,4), engine); - if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) + tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,4), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) onground = true; } velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)*movement / time; @@ -164,13 +165,13 @@ namespace MWWorld for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) { // trace to where character would go if there were no obstructions - actortrace(&trace, colobj, newPosition, newPosition+velocity*remainingTime, engine); + tracer.doTrace(colobj, newPosition, newPosition+velocity*remainingTime, engine); // check for obstructions - if(trace.fraction >= 1.0f) + if(tracer.mFraction >= 1.0f) { - newPosition = trace.endpos; - remainingTime *= (1.0f-trace.fraction); + newPosition = tracer.mEndPos; + remainingTime *= (1.0f-tracer.mFraction); break; } @@ -182,9 +183,9 @@ namespace MWWorld { // Can't move this way, try to find another spot along the plane Ogre::Real movelen = velocity.normalise(); - Ogre::Vector3 reflectdir = velocity.reflect(trace.planenormal); + Ogre::Vector3 reflectdir = velocity.reflect(tracer.mPlaneNormal); reflectdir.normalise(); - velocity = slide(reflectdir, trace.planenormal)*movelen; + velocity = slide(reflectdir, tracer.mPlaneNormal)*movelen; // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. @@ -195,9 +196,9 @@ namespace MWWorld if(onground) { - actortrace(&trace, colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+4.0f), engine); - if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) - newPosition.z = trace.endpos.z + 2.0f; + tracer.doTrace(colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+4.0f), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) + newPosition.z = tracer.mEndPos.z + 2.0f; else onground = false; } diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index 744462a0c..5edf53620 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -9,6 +9,11 @@ #include "physic.hpp" +namespace OEngine +{ +namespace Physic +{ + class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: @@ -47,7 +52,8 @@ protected: const btScalar mMinSlopeDot; }; -void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, OEngine::Physic::PhysicEngine *enginePass) + +void ActorTracer::doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, const PhysicEngine *enginePass) { const btVector3 btstart(start.x, start.y, start.z); const btVector3 btend(end.x, end.y, end.z); @@ -72,14 +78,17 @@ void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vec if(newTraceCallback.hasHit()) { const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; - results->fraction = newTraceCallback.m_closestHitFraction; - results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); - results->endpos = (end-start)*results->fraction + start; + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); + mEndPos = (end-start)*mFraction + start; } else { - results->endpos = end; - results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); - results->fraction = 1.0f; + mEndPos = end; + mPlaneNormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); + mFraction = 1.0f; } } + +} +} diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index 6353d6cfa..f81579c2e 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -4,26 +4,26 @@ #include -namespace OEngine -{ - namespace Physic - { - class PhysicEngine; - } -} - - class btCollisionObject; -struct traceResults +namespace OEngine +{ +namespace Physic { - Ogre::Vector3 endpos; - Ogre::Vector3 planenormal; + class PhysicEngine; + + struct ActorTracer + { + Ogre::Vector3 mEndPos; + Ogre::Vector3 mPlaneNormal; - float fraction; -}; + float mFraction; -void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); + void doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, + const PhysicEngine *enginePass); + }; +} +} #endif From 0481e64b0211310537de877b9e080c2d83ee0646 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 04:55:35 -0700 Subject: [PATCH 013/194] Fix tracing down --- apps/openmw/mwworld/physicssystem.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index ee6514590..1beb5a7e5 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -92,9 +92,10 @@ namespace MWWorld if (!physicActor) return position; - const int maxHeight = 64.f; - Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); + const Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); + Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, halfExtents.z); + const int maxHeight = 200.f; OEngine::Physic::ActorTracer tracer; tracer.doTrace(physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); if(tracer.mFraction >= 1.0f) @@ -103,7 +104,7 @@ namespace MWWorld physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); newPosition = tracer.mEndPos; - newPosition.z -= physicActor->getHalfExtents().z; + newPosition.z -= halfExtents.z; newPosition.z += 2.0f; return newPosition; From 96bab88da6c6aea589e86ac1da2c4c1aa6c08a16 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 07:48:45 -0700 Subject: [PATCH 014/194] Add physics methods to queue and apply movements --- apps/openmw/mwworld/physicssystem.cpp | 37 +++++++++++++++++++++++++-- apps/openmw/mwworld/physicssystem.hpp | 14 +++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 1beb5a7e5..0ae3e6fb2 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -427,8 +427,6 @@ namespace MWWorld void PhysicsSystem::removeObject (const std::string& handle) { - //TODO:check if actor??? - mEngine->removeCharacter(handle); mEngine->removeRigidBody(handle); mEngine->deleteRigidBody(handle); @@ -540,4 +538,39 @@ namespace MWWorld return true; } + + + void PhysicsSystem::queueObjectMovement(const Ptr &ptr, const Ogre::Vector3 &movement) + { + PtrVelocityList::iterator iter = mMovementQueue.begin(); + for(;iter != mMovementQueue.end();iter++) + { + if(iter->first == ptr) + { + iter->second = movement; + return; + } + } + + mMovementQueue.push_back(std::make_pair(ptr, movement)); + } + + const PtrVelocityList& PhysicsSystem::applyQueuedMovement(float dt) + { + mMovementResults.clear(); + + const MWBase::World *world = MWBase::Environment::get().getWorld(); + PtrVelocityList::iterator iter = mMovementQueue.begin(); + for(;iter != mMovementQueue.end();iter++) + { + Ogre::Vector3 newpos; + newpos = move(iter->first, iter->second, dt, + !world->isSwimming(iter->first) && !world->isFlying(iter->first)); + mMovementResults.push_back(std::make_pair(iter->first, newpos)); + } + + mMovementQueue.clear(); + + return mMovementResults; + } } diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 2b8cb00af..438e2ada6 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -5,6 +5,8 @@ #include +#include "ptr.hpp" + namespace OEngine { @@ -21,7 +23,8 @@ namespace OEngine namespace MWWorld { class World; - class Ptr; + + typedef std::vector > PtrVelocityList; class PhysicsSystem { @@ -80,12 +83,21 @@ namespace MWWorld bool getObjectAABB(const MWWorld::Ptr &ptr, Ogre::Vector3 &min, Ogre::Vector3 &max); + /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will + /// be overwritten. Valid until the next call to applyQueuedMovement. + void queueObjectMovement(const Ptr &ptr, const Ogre::Vector3 &velocity); + + const PtrVelocityList& applyQueuedMovement(float dt); + private: OEngine::Render::OgreRenderer &mRender; OEngine::Physic::PhysicEngine* mEngine; std::map handleToMesh; + PtrVelocityList mMovementQueue; + PtrVelocityList mMovementResults; + PhysicsSystem (const PhysicsSystem&); PhysicsSystem& operator= (const PhysicsSystem&); }; From 9d56e2d86d8019192da8e92f9323d2e062ec2b7e Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 22:34:38 -0700 Subject: [PATCH 015/194] Apply movement by queueing it to do later --- apps/openmw/mwbase/world.hpp | 5 +-- apps/openmw/mwmechanics/actors.cpp | 11 +----- apps/openmw/mwmechanics/actors.hpp | 2 -- apps/openmw/mwmechanics/character.cpp | 48 ++++++++++++--------------- apps/openmw/mwmechanics/character.hpp | 2 +- apps/openmw/mwmechanics/objects.cpp | 5 +-- apps/openmw/mwworld/physicssystem.cpp | 6 ++-- apps/openmw/mwworld/worldimp.cpp | 33 +++++++++--------- apps/openmw/mwworld/worldimp.hpp | 8 +++-- 9 files changed, 52 insertions(+), 68 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 4b4ffd0d3..38d8a0998 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -250,8 +250,9 @@ namespace MWBase virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const = 0; ///< Convert position to cell numbers - virtual void doPhysics (const MWWorld::PtrMovementList &actors, float duration) = 0; - ///< Run physics simulation and modify \a world accordingly. + virtual void queueMovement(const MWWorld::Ptr &ptr, const Ogre::Vector3 &velocity) = 0; + ///< Queues movement for \a ptr (in local space), to be applied in the next call to + /// doPhysics. virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0; ///< cast a Ray and return true if there is an object in the ray path. diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 10d609384..b8d0a8745 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -300,17 +300,8 @@ namespace MWMechanics if(!paused) { - mMovement.reserve(mActors.size()); - for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();++iter) - { - Movement movement; - iter->second->update(duration, movement); - mMovement.push_back(std::make_pair(iter->first, movement)); - } - MWBase::Environment::get().getWorld()->doPhysics(mMovement, duration); - - mMovement.clear(); + iter->second->update(duration); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index a289a9174..69878a000 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -28,8 +28,6 @@ namespace MWMechanics typedef std::map PtrControllerMap; PtrControllerMap mActors; - MWWorld::PtrMovementList mMovement; - std::map mDeathCount; float mDuration; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 1b220e65f..04e4185ba 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -663,9 +663,11 @@ bool CharacterController::updateNpcState(bool onground, bool inwater, bool isrun return forcestateupdate; } -void CharacterController::update(float duration, Movement &movement) +void CharacterController::update(float duration) { + MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Class &cls = MWWorld::Class::get(mPtr); + Ogre::Vector3 movement(0.0f); if(!cls.isActor()) { @@ -684,8 +686,6 @@ void CharacterController::update(float duration, Movement &movement) } else if(!cls.getCreatureStats(mPtr).isDead()) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - bool onground = world->isOnGround(mPtr); bool inwater = world->isSwimming(mPtr); bool isrunning = cls.getStance(mPtr, MWWorld::Class::Run); @@ -796,45 +796,41 @@ void CharacterController::update(float duration, Movement &movement) } } - vec *= duration; - movement.mPosition[0] += vec.x; - movement.mPosition[1] += vec.y; - movement.mPosition[2] += vec.z; - rot *= duration; - movement.mRotation[0] += rot.x; - movement.mRotation[1] += rot.y; - movement.mRotation[2] += rot.z; - if(cls.isNpc()) forcestateupdate = updateNpcState(onground, inwater, isrunning, sneak); refreshCurrentAnims(idlestate, movestate, forcestateupdate); + + rot *= duration * Ogre::Math::RadiansToDegrees(1.0f); + world->rotateObject(mPtr, rot.x, rot.y, rot.z, true); + + world->queueMovement(mPtr, vec); + movement = vec; } else if(cls.getCreatureStats(mPtr).isDead()) { MWBase::Environment::get().getWorld()->enableActorCollision(mPtr, false); + world->queueMovement(mPtr, Ogre::Vector3(0.0f)); } if(mAnimation && !mSkipAnim) { - Ogre::Vector3 moved = mAnimation->runAnimation(duration); + Ogre::Vector3 moved = mAnimation->runAnimation(duration) / duration; // Ensure we're moving in generally the right direction if(mMovementSpeed > 0.f) { - if((movement.mPosition[0] < 0.0f && movement.mPosition[0] < moved.x*2.0f) || - (movement.mPosition[0] > 0.0f && movement.mPosition[0] > moved.x*2.0f)) - moved.x = movement.mPosition[0]; - if((movement.mPosition[1] < 0.0f && movement.mPosition[1] < moved.y*2.0f) || - (movement.mPosition[1] > 0.0f && movement.mPosition[1] > moved.y*2.0f)) - moved.y = movement.mPosition[1]; - if((movement.mPosition[2] < 0.0f && movement.mPosition[2] < moved.z*2.0f) || - (movement.mPosition[2] > 0.0f && movement.mPosition[2] > moved.z*2.0f)) - moved.z = movement.mPosition[2]; + if((movement.x < 0.0f && movement.x < moved.x*2.0f) || + (movement.x > 0.0f && movement.x > moved.x*2.0f)) + moved.x = movement.x; + if((movement.y < 0.0f && movement.y < moved.y*2.0f) || + (movement.y > 0.0f && movement.y > moved.y*2.0f)) + moved.y = movement.y; + if((movement.z < 0.0f && movement.z < moved.z*2.0f) || + (movement.z > 0.0f && movement.z > moved.z*2.0f)) + moved.z = movement.z; } - - movement.mPosition[0] = moved.x; - movement.mPosition[1] = moved.y; - movement.mPosition[2] = moved.z; + if(moved.squaredLength() > 1.0f) + world->queueMovement(mPtr, moved); } mSkipAnim = false; } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 34d778fea..b631f9980 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -164,7 +164,7 @@ public: void updatePtr(const MWWorld::Ptr &ptr); - void update(float duration, Movement &movement); + void update(float duration); void playGroup(const std::string &groupname, int mode, int count); void skipAnim(); diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index e0c0c756b..694987855 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -65,10 +65,7 @@ void Objects::update(float duration, bool paused) if(!paused) { for(PtrControllerMap::iterator iter(mObjects.begin());iter != mObjects.end();++iter) - { - Movement movement; - iter->second->update(duration, movement); - } + iter->second->update(duration); } } diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 0ae3e6fb2..94eba704c 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -124,7 +124,7 @@ namespace MWWorld return position + (Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)* Ogre::Quaternion(Ogre::Radian(-refpos.rot[1]), Ogre::Vector3::UNIT_Y)* Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * - movement; + movement * time; } btCollisionObject *colobj = physicActor->getCollisionBody(); @@ -140,7 +140,7 @@ namespace MWWorld velocity = (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * - movement / time; + movement; } else { @@ -150,7 +150,7 @@ namespace MWWorld if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) onground = true; } - velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)*movement / time; + velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; velocity.z += physicActor->getVerticalForce(); } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index bd234c0b6..af3f35464 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1083,7 +1083,12 @@ namespace MWWorld --cellY; } - void World::doPhysics(const PtrMovementList &actors, float duration) + void World::queueMovement(const Ptr &ptr, const Vector3 &velocity) + { + mPhysics->queueObjectMovement(ptr, velocity); + } + + void World::doPhysics(float duration) { /* No duration? Shouldn't be any movement, then. */ if(duration <= 0.0f) @@ -1091,8 +1096,9 @@ namespace MWWorld processDoors(duration); - PtrMovementList::const_iterator player(actors.end()); - for(PtrMovementList::const_iterator iter(actors.begin());iter != actors.end();iter++) + const PtrVelocityList &results = mPhysics->applyQueuedMovement(duration); + PtrVelocityList::const_iterator player(results.end()); + for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();iter++) { if(iter->first.getRefData().getHandle() == "player") { @@ -1100,23 +1106,12 @@ namespace MWWorld player = iter; continue; } - - rotateObjectImp(iter->first, Ogre::Vector3(iter->second.mRotation), true); - - Ogre::Vector3 vec = mPhysics->move(iter->first, Ogre::Vector3(iter->second.mPosition), duration, - !isSwimming(iter->first) && !isFlying(iter->first)); - moveObjectImp(iter->first, vec.x, vec.y, vec.z); - } - if(player != actors.end()) - { - rotateObjectImp(player->first, Ogre::Vector3(player->second.mRotation), true); - - Ogre::Vector3 vec = mPhysics->move(player->first, Ogre::Vector3(player->second.mPosition), duration, - !isSwimming(player->first) && !isFlying(player->first)); - moveObjectImp(player->first, vec.x, vec.y, vec.z); + moveObjectImp(iter->first, iter->second.x, iter->second.y, iter->second.z); } + if(player != results.end()) + moveObjectImp(player->first, player->second.x, player->second.y, player->second.z); - mPhysEngine->stepSimulation (duration); + mPhysEngine->stepSimulation(duration); } bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2) @@ -1261,6 +1256,8 @@ namespace MWWorld mWorldScene->update (duration, paused); + doPhysics (duration); + performUpdateSceneQueries (); updateWindowManager (); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b2f6418a3..2f9629f57 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -106,6 +106,9 @@ namespace MWWorld void processDoors(float duration); ///< Run physics simulation and modify \a world accordingly. + void doPhysics(float duration); + ///< Run physics simulation and modify \a world accordingly. + void ensureNeededRecords(); int mPlayIntro; @@ -276,8 +279,9 @@ namespace MWWorld virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const; ///< Convert position to cell numbers - virtual void doPhysics(const PtrMovementList &actors, float duration); - ///< Run physics simulation and modify \a world accordingly. + virtual void queueMovement(const Ptr &ptr, const Ogre::Vector3 &velocity); + ///< Queues movement for \a ptr (in local space), to be applied in the next call to + /// doPhysics. virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2); ///< cast a Ray and return true if there is an object in the ray path. From c38860fa72936ea956a07d729c29e0ff6c392552 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 18 Aug 2013 14:17:18 +0200 Subject: [PATCH 016/194] added parser and custom filter edit widget (parser not functional yet; always returns a false boolean node) --- apps/opencs/CMakeLists.txt | 4 +-- apps/opencs/model/filter/parser.cpp | 33 +++++++++++++++++ apps/opencs/model/filter/parser.hpp | 39 +++++++++++++++++++++ apps/opencs/view/filter/editwidget.cpp | 20 +++++++++++ apps/opencs/view/filter/editwidget.hpp | 30 ++++++++++++++++ apps/opencs/view/filter/recordfilterbox.cpp | 5 +-- 6 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 apps/opencs/model/filter/parser.cpp create mode 100644 apps/opencs/model/filter/parser.hpp create mode 100644 apps/opencs/view/filter/editwidget.cpp create mode 100644 apps/opencs/view/filter/editwidget.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 8a1949e51..031c650df 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode + node unarynode narynode leafnode booleannode parser ) opencs_hdrs_noqt (model/filter @@ -116,7 +116,7 @@ opencs_hdrs_noqt (model/filter ) opencs_units (view/filter - filtercreator filterbox recordfilterbox + filtercreator filterbox recordfilterbox editwidget ) set (OPENCS_US diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp new file mode 100644 index 000000000..46052cec6 --- /dev/null +++ b/apps/opencs/model/filter/parser.cpp @@ -0,0 +1,33 @@ + +#include "parser.hpp" + +#include + +#include "booleannode.hpp" + +CSMFilter::Parser::Parser() : mState (State_Begin) {} + +void CSMFilter::Parser::parse (const std::string& filter) +{ + // reset + mState = State_Begin; + mFilter.reset(); + + + // for now we ignore the filter string + mFilter.reset (new BooleanNode (false)); + mState = State_End; +} + +CSMFilter::Parser::State CSMFilter::Parser::getState() const +{ + return mState; +} + +boost::shared_ptr CSMFilter::Parser::getFilter() const +{ + if (mState!=State_End) + throw std::logic_error ("No filter available"); + + return mFilter; +} \ No newline at end of file diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp new file mode 100644 index 000000000..72dc7d507 --- /dev/null +++ b/apps/opencs/model/filter/parser.hpp @@ -0,0 +1,39 @@ +#ifndef CSM_FILTER_PARSER_H +#define CSM_FILTER_PARSER_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class Parser + { + public: + + enum State + { + State_Begin, + State_End + }; + + private: + + State mState; + boost::shared_ptr mFilter; + + public: + + Parser(); + + void parse (const std::string& filter); + ///< Discards any previous calls to parse + + State getState() const; + + boost::shared_ptr getFilter() const; + ///< Throws an exception if getState()!=State_End + }; +} + +#endif diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp new file mode 100644 index 000000000..8fc4a88ae --- /dev/null +++ b/apps/opencs/view/filter/editwidget.cpp @@ -0,0 +1,20 @@ + +#include "editwidget.hpp" + +CSVFilter::EditWidget::EditWidget (QWidget *parent) +: QLineEdit (parent) +{ + connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); +} + +void CSVFilter::EditWidget::textChanged (const QString& text) +{ + mParser.parse (text.toUtf8().constData()); + + if (mParser.getState()==CSMFilter::Parser::State_End) + emit filterChanged(); + else + { + /// \todo error handling + } +} \ No newline at end of file diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp new file mode 100644 index 000000000..5959a8b16 --- /dev/null +++ b/apps/opencs/view/filter/editwidget.hpp @@ -0,0 +1,30 @@ +#ifndef CSV_FILTER_EDITWIDGET_H +#define CSV_FILTER_EDITWIDGET_H + +#include + +#include "../../model/filter/parser.hpp" + +namespace CSVFilter +{ + class EditWidget : public QLineEdit + { + Q_OBJECT + + CSMFilter::Parser mParser; + + public: + + EditWidget (QWidget *parent = 0); + + signals: + + void filterChanged(); + + private slots: + + void textChanged (const QString& text); + }; +} + +#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index b0c195664..5e297ec38 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -2,9 +2,10 @@ #include "recordfilterbox.hpp" #include -#include #include +#include "editwidget.hpp" + CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) : QWidget (parent) { @@ -14,7 +15,7 @@ CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) layout->addWidget (new QLabel ("Record Filter", this)); - layout->addWidget (new QLineEdit (this)); + layout->addWidget (new EditWidget (this)); setLayout (layout); } \ No newline at end of file From 8c3564326e73bb897f84084f2f4601b8af1e0174 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 18 Aug 2013 05:38:50 -0700 Subject: [PATCH 017/194] Improve movement inertia Handles all 3 axis. Incoming velocity is only added to inertia when leaving the ground, and does not continually add to it. --- apps/openmw/mwworld/physicssystem.cpp | 38 ++++++++++++++++++--------- libs/openengine/bullet/physic.cpp | 25 ++++++------------ libs/openengine/bullet/physic.hpp | 24 ++++++++++------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 94eba704c..bff7b28ab 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -132,8 +132,10 @@ namespace MWWorld position.z += halfExtents.z; OEngine::Physic::ActorTracer tracer; - bool onground = false; + bool wasOnGround = false; + bool isOnGround = false; const Ogre::Quaternion orient; // Don't rotate actor collision boxes + Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; if(!gravity) { @@ -144,17 +146,19 @@ namespace MWWorld } else { - if(!(movement.z > 0.0f)) + tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,4), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) { - tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,4), engine); - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) - onground = true; + wasOnGround = true; + if(!(movement.z > 0.0f)) + isOnGround = true; } - velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; - velocity.z += physicActor->getVerticalForce(); + inertia = physicActor->getInertialForce(); + velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; + velocity += inertia; } - if(onground) + if(isOnGround) { // if we're on the ground, don't try to fall velocity.z = std::max(0.0f, velocity.z); @@ -179,7 +183,7 @@ namespace MWWorld //std::cout<<"angle: "<setOnGround(onground); - physicActor->setVerticalForce(!onground ? velocity.z - time*627.2f : 0.0f); + physicActor->setOnGround(isOnGround); + if(isOnGround) + physicActor->setInertialForce(Ogre::Vector3(0.0f)); + else + { + if(wasOnGround) + inertia = velocity; + inertia.z -= time*627.2f; + physicActor->setInertialForce(inertia); + } newPosition.z -= halfExtents.z; return newPosition; diff --git a/libs/openengine/bullet/physic.cpp b/libs/openengine/bullet/physic.cpp index ba47b6e64..e488d6e99 100644 --- a/libs/openengine/bullet/physic.cpp +++ b/libs/openengine/bullet/physic.cpp @@ -19,7 +19,8 @@ namespace Physic PhysicActor::PhysicActor(const std::string &name, const std::string &mesh, PhysicEngine *engine, const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, float scale) : mName(name), mEngine(engine), mMesh(mesh), mBoxScaledTranslation(0,0,0), mBoxRotationInverse(0,0,0,0) - , mBody(0), mRaycastingBody(0), onGround(false), collisionMode(true), mBoxRotation(0,0,0,0), verticalForce(0.0f) + , mBody(0), mRaycastingBody(0), mOnGround(false), mCollisionMode(true), mBoxRotation(0,0,0,0) + , mForce(0.0f) { mBody = mEngine->createAndAdjustRigidBody(mMesh, mName, scale, position, rotation, &mBoxScaledTranslation, &mBoxRotation); mRaycastingBody = mEngine->createAndAdjustRigidBody(mMesh, mName, scale, position, rotation, &mBoxScaledTranslation, &mBoxRotation, true); @@ -45,9 +46,9 @@ namespace Physic void PhysicActor::enableCollisions(bool collision) { assert(mBody); - if(collision && !collisionMode) enableCollisionBody(); - if(!collision && collisionMode) disableCollisionBody(); - collisionMode = collision; + if(collision && !mCollisionMode) enableCollisionBody(); + if(!collision && mCollisionMode) disableCollisionBody(); + mCollisionMode = collision; } @@ -123,24 +124,14 @@ namespace Physic return Ogre::Vector3(0.0f); } - void PhysicActor::setVerticalForce(float force) + void PhysicActor::setInertialForce(const Ogre::Vector3 &force) { - verticalForce = force; - } - - float PhysicActor::getVerticalForce() const - { - return verticalForce; + mForce = force; } void PhysicActor::setOnGround(bool grounded) { - onGround = grounded; - } - - bool PhysicActor::getOnGround() const - { - return collisionMode && onGround; + mOnGround = grounded; } void PhysicActor::disableCollisionBody() diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index 9b1541ea2..46dafda76 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -104,7 +104,7 @@ namespace Physic bool getCollisionMode() const { - return collisionMode; + return mCollisionMode; } @@ -130,18 +130,24 @@ namespace Physic Ogre::Vector3 getHalfExtents() const; /** - * Sets the current amount of vertical force (gravity) affecting this physic actor + * Sets the current amount of inertial force (incl. gravity) affecting this physic actor */ - void setVerticalForce(float force); + void setInertialForce(const Ogre::Vector3 &force); /** - * Gets the current amount of vertical force (gravity) affecting this physic actor + * Gets the current amount of inertial force (incl. gravity) affecting this physic actor */ - float getVerticalForce() const; + const Ogre::Vector3 &getInertialForce() const + { + return mForce; + } void setOnGround(bool grounded); - bool getOnGround() const; + bool getOnGround() const + { + return mCollisionMode && mOnGround; + } btCollisionObject *getCollisionBody() const { @@ -169,9 +175,9 @@ public: Ogre::Quaternion mBoxRotation; btQuaternion mBoxRotationInverse; - float verticalForce; - bool onGround; - bool collisionMode; + Ogre::Vector3 mForce; + bool mOnGround; + bool mCollisionMode; std::string mMesh; std::string mName; From b0f8045c7207f36941e2f10e73ff2aaf742f6b9b Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 18 Aug 2013 05:59:06 -0700 Subject: [PATCH 018/194] Improve mid-air control --- apps/openmw/mwmechanics/character.cpp | 68 +++++++++++++++++---------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 04e4185ba..90dd8653e 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -690,38 +690,17 @@ void CharacterController::update(float duration) bool inwater = world->isSwimming(mPtr); bool isrunning = cls.getStance(mPtr, MWWorld::Class::Run); bool sneak = cls.getStance(mPtr, MWWorld::Class::Sneak); + bool flying = world->isFlying(mPtr); Ogre::Vector3 vec = cls.getMovementVector(mPtr); Ogre::Vector3 rot = cls.getRotationVector(mPtr); mMovementSpeed = cls.getSpeed(mPtr); - CharacterState movestate = CharState_None; - CharacterState idlestate = CharState_SpecialIdle; - bool forcestateupdate = false; - vec.x *= mMovementSpeed; vec.y *= mMovementSpeed; - /* FIXME: The state should be set to Jump, and X/Y movement should be disallowed except - * for the initial thrust (which would be carried by "physics" until landing). */ - if(!onground || sneak) - vec.z = 0.0f; - else if(vec.z > 0.0f) - { - float z = cls.getJump(mPtr); - - if(vec.x == 0 && vec.y == 0) - vec.z *= z; - else - { - /* FIXME: this would be more correct if we were going into a jumping state, - * rather than normal walking/idle states. */ - //Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); - //vec *= Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; - vec.z *= z * 0.707f; - } - - //decrease fatigue by fFatigueJumpBase + (1 - normalizedEncumbrance) * fFatigueJumpMult; - } + CharacterState movestate = CharState_None; + CharacterState idlestate = CharState_SpecialIdle; + bool forcestateupdate = false; isrunning = isrunning && std::abs(vec[0])+std::abs(vec[1]) > 0.0f; @@ -748,7 +727,44 @@ void CharacterController::update(float duration) } } - if(std::abs(vec.x/2.0f) > std::abs(vec.y)) + if(sneak || inwater || flying) + vec.z = 0.0f; + + if(!onground && !flying && !inwater) + { + const MWWorld::Store &gmst = world->getStore().get(); + + // This is a guess. All that seems to be known is that "While the player is in the + // air, fJumpMoveBase and fJumpMoveMult governs air control." Assuming Acrobatics + // plays a role, this makes the most sense. + float mult = 0.0f; + if(cls.isNpc()) + { + const NpcStats &stats = cls.getNpcStats(mPtr); + mult = gmst.find("fJumpMoveBase")->getFloat() + + (stats.getSkill(ESM::Skill::Acrobatics).getModified()/100.0f * + gmst.find("fJumpMoveMult")->getFloat()); + } + + vec.x *= mult; + vec.y *= mult; + vec.z = 0.0f; + } + else if(vec.z > 0.0f) + { + float z = cls.getJump(mPtr); + + if(vec.x == 0 && vec.y == 0) + vec = Ogre::Vector3(0.0f, 0.0f, z); + else + { + Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); + vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; + } + + //decrease fatigue by fFatigueJumpBase + (1 - normalizedEncumbrance) * fFatigueJumpMult; + } + else if(std::abs(vec.x/2.0f) > std::abs(vec.y)) { if(vec.x > 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) From 470f890a9add6916ef6b26160c7063de083c24da Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 18 Aug 2013 16:52:51 +0200 Subject: [PATCH 019/194] fixed BooleanNode constructor --- apps/opencs/model/filter/booleannode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp index cea130d24..2f521ed06 100644 --- a/apps/opencs/model/filter/booleannode.cpp +++ b/apps/opencs/model/filter/booleannode.cpp @@ -1,7 +1,7 @@ #include "booleannode.hpp" -CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true) {} +CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} bool CSMFilter::BooleanNode::test (const CSMWorld::IdTable& table, int row, const std::map& otherFilters, From ea8b9ce45b76923a4c52a66fe3f1c3b3d9d3d114 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 18 Aug 2013 16:53:28 +0200 Subject: [PATCH 020/194] apply filter to table after filter text change --- apps/opencs/model/filter/node.hpp | 6 +++++ apps/opencs/model/world/idtableproxymodel.cpp | 23 +++++++++++++++++++ apps/opencs/model/world/idtableproxymodel.hpp | 16 ++++++++++++- apps/opencs/view/filter/editwidget.cpp | 2 +- apps/opencs/view/filter/editwidget.hpp | 6 ++++- apps/opencs/view/filter/filterbox.cpp | 8 ++++++- apps/opencs/view/filter/filterbox.hpp | 7 ++++++ apps/opencs/view/filter/recordfilterbox.cpp | 10 ++++++-- apps/opencs/view/filter/recordfilterbox.hpp | 9 ++++++++ apps/opencs/view/world/table.cpp | 6 +++++ apps/opencs/view/world/table.hpp | 4 ++++ apps/opencs/view/world/tablesubview.cpp | 8 ++++++- 12 files changed, 98 insertions(+), 7 deletions(-) diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp index 09bc3cd42..9743034ba 100644 --- a/apps/opencs/model/filter/node.hpp +++ b/apps/opencs/model/filter/node.hpp @@ -5,6 +5,10 @@ #include #include +#include + +#include + namespace CSMWorld { class IdTable; @@ -55,4 +59,6 @@ namespace CSMFilter }; } +Q_DECLARE_METATYPE (boost::shared_ptr) + #endif diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index e99e1575c..e2c862270 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -1,8 +1,23 @@ #include "idtableproxymodel.hpp" +#include +#include + #include "idtable.hpp" +bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) + const +{ + if (!mFilter) + return true; + + std::map otherFilters; /// \todo get other filters; + std::map columns; /// \todo get columns + + return mFilter->test (dynamic_cast (*sourceModel()), sourceRow, otherFilters, columns, mUserValue); +} + CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) : QSortFilterProxyModel (parent) {} @@ -10,4 +25,12 @@ CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, int column) const { return mapFromSource (dynamic_cast (*sourceModel()).getModelIndex (id, column)); +} + +void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter, + const std::string& userValue) +{ + mFilter = filter; + mUserValue = userValue; + invalidateFilter(); } \ No newline at end of file diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 200b99fe2..c7dcd345a 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -1,9 +1,13 @@ #ifndef CSM_WOLRD_IDTABLEPROXYMODEL_H #define CSM_WOLRD_IDTABLEPROXYMODEL_H +#include + +#include + #include -#include +#include "../filter/node.hpp" namespace CSMWorld { @@ -11,11 +15,21 @@ namespace CSMWorld { Q_OBJECT + boost::shared_ptr mFilter; + std::string mUserValue; + + private: + + bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const; + public: IdTableProxyModel (QObject *parent = 0); virtual QModelIndex getModelIndex (const std::string& id, int column) const; + + void setFilter (const boost::shared_ptr& filter, + const std::string& userValue); }; } diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 8fc4a88ae..b8853ffa3 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -12,7 +12,7 @@ void CSVFilter::EditWidget::textChanged (const QString& text) mParser.parse (text.toUtf8().constData()); if (mParser.getState()==CSMFilter::Parser::State_End) - emit filterChanged(); + emit filterChanged (mParser.getFilter(), ""); else { /// \todo error handling diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index 5959a8b16..9c4b1fa16 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -1,9 +1,12 @@ #ifndef CSV_FILTER_EDITWIDGET_H #define CSV_FILTER_EDITWIDGET_H +#include + #include #include "../../model/filter/parser.hpp" +#include "../../model/filter/node.hpp" namespace CSVFilter { @@ -19,7 +22,8 @@ namespace CSVFilter signals: - void filterChanged(); + void filterChanged (boost::shared_ptr filter, + const std::string& userValue); private slots: diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index f3f17706b..9da08d3ba 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -12,7 +12,13 @@ CSVFilter::FilterBox::FilterBox (QWidget *parent) layout->setContentsMargins (0, 0, 0, 0); - layout->addWidget (new RecordFilterBox (this)); + RecordFilterBox *recordFilterBox = new RecordFilterBox (this); + + layout->addWidget (recordFilterBox); setLayout (layout); + + connect (recordFilterBox, + SIGNAL (filterChanged (boost::shared_ptr, const std::string&)), + this, SIGNAL (recordFilterChanged (boost::shared_ptr, const std::string&))); } \ No newline at end of file diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index 969a43cd7..5f260003a 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -3,6 +3,8 @@ #include +#include "../../model/filter/node.hpp" + namespace CSVFilter { class FilterBox : public QWidget @@ -12,6 +14,11 @@ namespace CSVFilter public: FilterBox (QWidget *parent = 0); + + signals: + + void recordFilterChanged (boost::shared_ptr filter, + const std::string& userValue); }; } diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index 5e297ec38..c4b516f03 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -15,7 +15,13 @@ CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) layout->addWidget (new QLabel ("Record Filter", this)); - layout->addWidget (new EditWidget (this)); + EditWidget *editWidget = new EditWidget (this); + + layout->addWidget (editWidget); setLayout (layout); -} \ No newline at end of file + + connect ( + editWidget, SIGNAL (filterChanged (boost::shared_ptr, const std::string&)), + this, SIGNAL (filterChanged (boost::shared_ptr, const std::string&))); +} diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 3a411f808..8fc1e263b 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -1,10 +1,14 @@ #ifndef CSV_FILTER_RECORDFILTERBOX_H #define CSV_FILTER_RECORDFILTERBOX_H +#include + #include #include +#include "../../model/filter/node.hpp" + namespace CSVFilter { class RecordFilterBox : public QWidget @@ -14,6 +18,11 @@ namespace CSVFilter public: RecordFilterBox (QWidget *parent = 0); + + signals: + + void filterChanged (boost::shared_ptr filter, + const std::string& userValue); }; } diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 4ae25d10b..cc84e612d 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -292,4 +292,10 @@ void CSVWorld::Table::requestFocus (const std::string& id) if (index.isValid()) scrollTo (index, QAbstractItemView::PositionAtTop); +} + +void CSVWorld::Table::recordFilterChanged (boost::shared_ptr filter, + const std::string& userValue) +{ + mProxyModel->setFilter (filter, userValue); } \ No newline at end of file diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 0c24e7b54..2e24e46a5 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -6,6 +6,8 @@ #include +#include "../../model/filter/node.hpp" + class QUndoStack; class QAction; @@ -85,6 +87,8 @@ namespace CSVWorld void requestFocus (const std::string& id); + void recordFilterChanged (boost::shared_ptr filter, + const std::string& userValue); }; } diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index df95940c9..cb6f430c1 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -25,7 +25,9 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D layout->insertWidget (0, mTable = new Table (id, document.getData(), document.getUndoStack(), mBottom->canCreateAndDelete()), 2); - layout->insertWidget (0, new CSVFilter::FilterBox (this)); + CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (this); + + layout->insertWidget (0, filterBox); QWidget *widget = new QWidget; @@ -48,6 +50,10 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D connect (mBottom, SIGNAL (requestFocus (const std::string&)), mTable, SLOT (requestFocus (const std::string&))); + + connect (filterBox, + SIGNAL (recordFilterChanged (boost::shared_ptr, const std::string&)), + mTable, SLOT (recordFilterChanged (boost::shared_ptr, const std::string&))); } void CSVWorld::TableSubView::setEditLock (bool locked) From 7770203dff236853212d5b05746cddb8e28c211d Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 18 Aug 2013 08:24:39 -0700 Subject: [PATCH 021/194] Some physics cleanup Gets rid of some unneeded/unused variables, and halves the 'on ground' offset. --- apps/openmw/mwworld/physicssystem.cpp | 36 ++++++++++++--------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index bff7b28ab..a4d2aa321 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -45,23 +45,23 @@ namespace MWWorld const Ogre::Vector3 &velocity, float &remainingTime, OEngine::Physic::PhysicEngine *engine) { - OEngine::Physic::ActorTracer tracer; - tracer.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(tracer.mFraction == 0.0f) + OEngine::Physic::ActorTracer tracer, stepper; + + stepper.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(stepper.mFraction == 0.0f) return false; - tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos + velocity*remainingTime, engine); + tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + velocity*remainingTime, engine); if(tracer.mFraction < std::numeric_limits::epsilon() || (tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) > sMaxSlope)) return false; - float movefrac = tracer.mFraction; - tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(getSlope(tracer.mPlaneNormal) <= sMaxSlope) + stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(getSlope(stepper.mPlaneNormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - position = tracer.mEndPos; - remainingTime *= (1.0f-movefrac); + position = stepper.mEndPos; + remainingTime *= (1.0f-tracer.mFraction); return true; } @@ -132,9 +132,7 @@ namespace MWWorld position.z += halfExtents.z; OEngine::Physic::ActorTracer tracer; - bool wasOnGround = false; bool isOnGround = false; - const Ogre::Quaternion orient; // Don't rotate actor collision boxes Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; if(!gravity) @@ -146,11 +144,10 @@ namespace MWWorld } else { - tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,4), engine); - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) + if(!(movement.z > 0.0f)) { - wasOnGround = true; - if(!(movement.z > 0.0f)) + tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,2), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) isOnGround = true; } inertia = physicActor->getInertialForce(); @@ -164,7 +161,6 @@ namespace MWWorld velocity.z = std::max(0.0f, velocity.z); } - const Ogre::Vector3 up(0.0f, 0.0f, 1.0f); Ogre::Vector3 newPosition = position; float remainingTime = time; for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) @@ -201,23 +197,23 @@ namespace MWWorld if(isOnGround) { - tracer.doTrace(colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+4.0f), engine); + tracer.doTrace(colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+2.0f), engine); if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) - newPosition.z = tracer.mEndPos.z + 2.0f; + newPosition.z = tracer.mEndPos.z + 1.0f; else isOnGround = false; } - physicActor->setOnGround(isOnGround); if(isOnGround) physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { - if(wasOnGround) + if(physicActor->getOnGround()) inertia = velocity; inertia.z -= time*627.2f; physicActor->setInertialForce(inertia); } + physicActor->setOnGround(isOnGround); newPosition.z -= halfExtents.z; return newPosition; From c7e97a83e195891c758fd8175f80986d9b4f42a5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 18 Aug 2013 17:28:04 +0200 Subject: [PATCH 022/194] automatically build column map on filter change --- apps/opencs/model/world/idtableproxymodel.cpp | 22 ++++++++++++++++--- apps/opencs/model/world/idtableproxymodel.hpp | 5 +++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index e2c862270..4bb440ce7 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -1,11 +1,26 @@ #include "idtableproxymodel.hpp" -#include #include #include "idtable.hpp" +void CSMWorld::IdTableProxyModel::updateColumnMap() +{ + mColumnMap.clear(); + + if (mFilter) + { + std::vector columns = mFilter->getReferencedColumns(); + + const IdTable& table = dynamic_cast (*sourceModel()); + + for (std::vector::const_iterator iter (columns.begin()); iter!=columns.end(); ++iter) + mColumnMap.insert (std::make_pair (*iter, + table.searchColumnIndex (static_cast (*iter)))); + } +} + bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const { @@ -13,9 +28,9 @@ bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelI return true; std::map otherFilters; /// \todo get other filters; - std::map columns; /// \todo get columns - return mFilter->test (dynamic_cast (*sourceModel()), sourceRow, otherFilters, columns, mUserValue); + return mFilter->test ( + dynamic_cast (*sourceModel()), sourceRow, otherFilters, mColumnMap, mUserValue); } CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) @@ -32,5 +47,6 @@ void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr +#include + #include #include "../filter/node.hpp" @@ -17,9 +19,12 @@ namespace CSMWorld boost::shared_ptr mFilter; std::string mUserValue; + std::map mColumnMap; // column ID, column index in this model (or -1) private: + void updateColumnMap(); + bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const; public: From c87a2794449b69eec4489b5f291188938e8e3d3c Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 18 Aug 2013 17:54:18 +0200 Subject: [PATCH 023/194] added error reporting in the filter edit widget (change the text colour for now) --- apps/opencs/model/filter/parser.cpp | 50 ++++++++++++++++++++++++-- apps/opencs/model/filter/parser.hpp | 8 +++++ apps/opencs/view/filter/editwidget.cpp | 10 +++++- apps/opencs/view/filter/editwidget.hpp | 2 ++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 46052cec6..06a974bbe 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -5,6 +5,37 @@ #include "booleannode.hpp" +namespace CSMFilter +{ + struct Token + { + enum Type + { + Type_EOS, + Type_None + }; + + Type mType; + + Token (Type type); + }; + + Token::Token (Type type) : mType (type) {} +} + +CSMFilter::Token CSMFilter::Parser::getNextToken (const std::string& filter, int& index) const +{ + if (index>=static_cast (filter.size())) + return Token (Token::Type_EOS); + + return Token (Token::Type_None); +} + +bool CSMFilter::Parser::isEndState() const +{ + return mState==State_End || mState==State_UnexpectedCharacter; +} + CSMFilter::Parser::Parser() : mState (State_Begin) {} void CSMFilter::Parser::parse (const std::string& filter) @@ -12,11 +43,24 @@ void CSMFilter::Parser::parse (const std::string& filter) // reset mState = State_Begin; mFilter.reset(); + int index = 0; + + while (!isEndState()) + { + Token token = getNextToken (filter, index); + switch (token.mType) + { + case Token::Type_None: mState = State_UnexpectedCharacter; break; + case Token::Type_EOS: mState = State_End; break; + } + } - // for now we ignore the filter string - mFilter.reset (new BooleanNode (false)); - mState = State_End; + if (mState==State_End && !mFilter) + { + // Empty filter string equals to filter "true". + mFilter.reset (new BooleanNode (true)); + } } CSMFilter::Parser::State CSMFilter::Parser::getState() const diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 72dc7d507..2d616b9e1 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -7,6 +7,8 @@ namespace CSMFilter { + struct Token; + class Parser { public: @@ -14,6 +16,7 @@ namespace CSMFilter enum State { State_Begin, + State_UnexpectedCharacter, State_End }; @@ -22,6 +25,11 @@ namespace CSMFilter State mState; boost::shared_ptr mFilter; + Token getNextToken (const std::string& filter, int& index) const; + + bool isEndState() const; + ///< This includes error states. + public: Parser(); diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index b8853ffa3..79cbc1dce 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -4,6 +4,7 @@ CSVFilter::EditWidget::EditWidget (QWidget *parent) : QLineEdit (parent) { + mPalette = palette(); connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); } @@ -12,9 +13,16 @@ void CSVFilter::EditWidget::textChanged (const QString& text) mParser.parse (text.toUtf8().constData()); if (mParser.getState()==CSMFilter::Parser::State_End) + { + setPalette (mPalette); emit filterChanged (mParser.getFilter(), ""); + } else { - /// \todo error handling + QPalette palette (mPalette); + palette.setColor (QPalette::Text, Qt::red); + setPalette (palette); + + /// \todo improve error reporting; mark only the faulty part } } \ No newline at end of file diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index 9c4b1fa16..4e692fc4d 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -4,6 +4,7 @@ #include #include +#include #include "../../model/filter/parser.hpp" #include "../../model/filter/node.hpp" @@ -15,6 +16,7 @@ namespace CSVFilter Q_OBJECT CSMFilter::Parser mParser; + QPalette mPalette; public: From 6e9f15793d8c0e937d890d8cbce78b613e358bcc Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 18 Aug 2013 23:42:56 -0700 Subject: [PATCH 024/194] Implement a jumping state --- apps/openmw/mwmechanics/character.cpp | 76 +++++++++++++++++---------- apps/openmw/mwmechanics/character.hpp | 8 +++ 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 90dd8653e..aa37f08a0 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -99,6 +99,8 @@ static const StateInfo sMovementList[] = { { CharState_SneakLeft, "sneakleft" }, { CharState_SneakRight, "sneakright" }, + { CharState_Jump, "jump" }, + { CharState_TurnLeft, "turnleft" }, { CharState_TurnRight, "turnright" }, }; @@ -214,7 +216,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat /* If we're playing the same animation, restart from the loop start instead of the * beginning. */ - int mode = ((movement != mCurrentMovement) ? 1 : 2); + int mode = ((movement == mCurrentMovement) ? 2 : 1); mAnimation->disable(mCurrentMovement); mCurrentMovement = movement; @@ -224,7 +226,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(mCurrentMovement)) > 1.0f) speedmult = mMovementSpeed / vel; mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false, - speedmult, ((mode==2)?"loop start":"start"), "stop", 0.0f, ~0ul); + speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul); } } } @@ -307,6 +309,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mMovementSpeed(0.0f) , mDeathState(CharState_None) , mUpperBodyState(UpperCharState_Nothing) + , mJumpState(JumpState_None) , mWeaponType(WeapType_None) , mSkipAnim(false) , mSecondsOfRunning(0) @@ -734,6 +737,8 @@ void CharacterController::update(float duration) { const MWWorld::Store &gmst = world->getStore().get(); + mJumpState = JumpState_Falling; + // This is a guess. All that seems to be known is that "While the player is in the // air, fJumpMoveBase and fJumpMoveMult governs air control." Assuming Acrobatics // plays a role, this makes the most sense. @@ -750,10 +755,11 @@ void CharacterController::update(float duration) vec.y *= mult; vec.z = 0.0f; } - else if(vec.z > 0.0f) + else if(vec.z > 0.0f && mJumpState == JumpState_None) { - float z = cls.getJump(mPtr); + mJumpState = JumpState_Falling; + float z = cls.getJump(mPtr); if(vec.x == 0 && vec.y == 0) vec = Ogre::Vector3(0.0f, 0.0f, z); else @@ -764,34 +770,46 @@ void CharacterController::update(float duration) //decrease fatigue by fFatigueJumpBase + (1 - normalizedEncumbrance) * fFatigueJumpMult; } - else if(std::abs(vec.x/2.0f) > std::abs(vec.y)) + else if(mJumpState == JumpState_Falling) { - if(vec.x > 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) - : (sneak ? CharState_SneakRight - : (isrunning ? CharState_RunRight : CharState_WalkRight))); - else if(vec.x < 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) - : (sneak ? CharState_SneakLeft - : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); - } - else if(vec.y != 0.0f) - { - if(vec.y > 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) - : (sneak ? CharState_SneakForward - : (isrunning ? CharState_RunForward : CharState_WalkForward))); - else if(vec.y < 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) - : (sneak ? CharState_SneakBack - : (isrunning ? CharState_RunBack : CharState_WalkBack))); + mJumpState = JumpState_Landing; + vec.z = 0.0f; } - else if(rot.z != 0.0f && !inwater && !sneak) + else { - if(rot.z > 0.0f) - movestate = CharState_TurnRight; - else if(rot.z < 0.0f) - movestate = CharState_TurnLeft; + if(!(vec.z > 0.0f)) + mJumpState = JumpState_None; + vec.z = 0.0f; + + if(std::abs(vec.x/2.0f) > std::abs(vec.y)) + { + if(vec.x > 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) + : (sneak ? CharState_SneakRight + : (isrunning ? CharState_RunRight : CharState_WalkRight))); + else if(vec.x < 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) + : (sneak ? CharState_SneakLeft + : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); + } + else if(vec.y != 0.0f) + { + if(vec.y > 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) + : (sneak ? CharState_SneakForward + : (isrunning ? CharState_RunForward : CharState_WalkForward))); + else if(vec.y < 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) + : (sneak ? CharState_SneakBack + : (isrunning ? CharState_RunBack : CharState_WalkBack))); + } + else if(rot.z != 0.0f && !inwater && !sneak) + { + if(rot.z > 0.0f) + movestate = CharState_TurnRight; + else if(rot.z < 0.0f) + movestate = CharState_TurnLeft; + } } if(movestate != CharState_None) diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index b631f9980..ba418eb6a 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -115,6 +115,12 @@ enum UpperBodyCharacterState { UpperCharState_CastingSpell }; +enum JumpingState { + JumpState_None, + JumpState_Falling, + JumpState_Landing +}; + class CharacterController { MWWorld::Ptr mPtr; @@ -135,6 +141,8 @@ class CharacterController UpperBodyCharacterState mUpperBodyState; + JumpingState mJumpState; + WeaponType mWeaponType; std::string mCurrentWeapon; From 1aa92067c2f42809c3025a513a2072abf311a4ec Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 04:56:02 -0700 Subject: [PATCH 025/194] Fix tracing down --- apps/openmw/mwworld/physicssystem.cpp | 14 ++++----- libs/openengine/bullet/trace.cpp | 42 +++++++++++++++++++++++++-- libs/openengine/bullet/trace.h | 2 ++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index a4d2aa321..c7294b4f9 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -92,22 +92,18 @@ namespace MWWorld if (!physicActor) return position; - const Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); - Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, halfExtents.z); - const int maxHeight = 200.f; OEngine::Physic::ActorTracer tracer; - tracer.doTrace(physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); + tracer.findGround(physicActor->getCollisionBody(), position, position-Ogre::Vector3(0,0,maxHeight), engine); if(tracer.mFraction >= 1.0f) + { + physicActor->setOnGround(false); return position; + } physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); - newPosition = tracer.mEndPos; - newPosition.z -= halfExtents.z; - newPosition.z += 2.0f; - - return newPosition; + return tracer.mEndPos; } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index 5edf53620..afda52448 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -65,9 +65,8 @@ void ActorTracer::doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, to.setOrigin(btend); ClosestNotMeConvexResultCallback newTraceCallback(actor, btstart-btend, btScalar(0.0)); - newTraceCallback.m_collisionFilterMask = OEngine::Physic::CollisionType_World | - OEngine::Physic::CollisionType_HeightMap | - OEngine::Physic::CollisionType_Actor; + newTraceCallback.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap | + CollisionType_Actor; btCollisionShape *shape = actor->getCollisionShape(); assert(shape->isConvex()); @@ -90,5 +89,42 @@ void ActorTracer::doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, } } +void ActorTracer::findGround(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, const PhysicEngine *enginePass) +{ + const btVector3 btstart(start.x, start.y, start.z+1.0f); + const btVector3 btend(end.x, end.y, end.z+1.0f); + + const btTransform &trans = actor->getWorldTransform(); + btTransform from(trans.getBasis(), btstart); + btTransform to(trans.getBasis(), btend); + + ClosestNotMeConvexResultCallback newTraceCallback(actor, btstart-btend, btScalar(0.0)); + newTraceCallback.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap | + CollisionType_Actor; + + const btBoxShape *shape = dynamic_cast(actor->getCollisionShape()); + assert(shape); + + btVector3 halfExtents = shape->getHalfExtentsWithMargin(); + halfExtents[2] = 1.0f; + btBoxShape box(halfExtents); + + enginePass->dynamicsWorld->convexSweepTest(&box, from, to, newTraceCallback); + if(newTraceCallback.hasHit()) + { + const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); + mEndPos = (end-start)*mFraction + start; + mEndPos[2] -= 1.0f; + } + else + { + mEndPos = end; + mPlaneNormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); + mFraction = 1.0f; + } +} + } } diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index f81579c2e..92795c87f 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -22,6 +22,8 @@ namespace Physic void doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, const PhysicEngine *enginePass); + void findGround(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, + const PhysicEngine *enginePass); }; } } From 3ca4d54bf999732703204eb6d58ae9ab23bbce22 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 07:26:42 -0700 Subject: [PATCH 026/194] Better handle animations with a 0-length loop --- apps/openmw/mwrender/animation.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 37cebcf0b..f44f53a74 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -833,7 +833,12 @@ Ogre::Vector3 Animation::runAnimation(float duration) float timepassed = duration * state.mSpeedMult; while(state.mPlaying) { - float targetTime = state.mTime + timepassed; + float targetTime; + + if(state.mTime >= state.mLoopStopTime && state.mLoopCount > 0) + goto handle_loop; + + targetTime = state.mTime + timepassed; if(textkey == textkeys.end() || textkey->first > targetTime) { if(mNonAccumCtrl && stateiter->first == mAnimationValuePtr[0]->getAnimName()) @@ -858,11 +863,10 @@ Ogre::Vector3 Animation::runAnimation(float duration) if(state.mTime >= state.mLoopStopTime && state.mLoopCount > 0) { + handle_loop: state.mLoopCount--; state.mTime = state.mLoopStartTime; state.mPlaying = true; - if(state.mTime >= state.mLoopStopTime) - break; textkey = textkeys.lower_bound(state.mTime); while(textkey != textkeys.end() && textkey->first <= state.mTime) @@ -870,6 +874,9 @@ Ogre::Vector3 Animation::runAnimation(float duration) handleTextKey(state, stateiter->first, textkey); textkey++; } + + if(state.mTime >= state.mLoopStopTime) + break; } if(timepassed <= 0.0f) From 48e594b7c4b866fe5d1aa4187d9e5f22b29b6376 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 08:09:23 -0700 Subject: [PATCH 027/194] Improve stepping down when starting on the ground --- apps/openmw/mwworld/physicssystem.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index c7294b4f9..48d397d31 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -128,6 +128,7 @@ namespace MWWorld position.z += halfExtents.z; OEngine::Physic::ActorTracer tracer; + bool wasOnGround = false; bool isOnGround = false; Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; @@ -140,15 +141,22 @@ namespace MWWorld } else { + velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; + if(physicActor->getOnGround()) + inertia = velocity; + else + { + inertia = physicActor->getInertialForce(); + velocity += inertia; + } + if(!(movement.z > 0.0f)) { + wasOnGround = physicActor->getOnGround(); tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,2), engine); if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) isOnGround = true; } - inertia = physicActor->getInertialForce(); - velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; - velocity += inertia; } if(isOnGround) @@ -172,10 +180,9 @@ namespace MWWorld break; } - //std::cout<<"angle: "<setInertialForce(Ogre::Vector3(0.0f)); else { - if(physicActor->getOnGround()) - inertia = velocity; inertia.z -= time*627.2f; physicActor->setInertialForce(inertia); } From ac3d3df9fc69289f56d3f3e7c9106adae969b526 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 08:10:18 -0700 Subject: [PATCH 028/194] Implement jumping animations --- apps/openmw/mwmechanics/character.cpp | 41 ++++++++++++++++++++++++++- apps/openmw/mwmechanics/character.hpp | 2 ++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index aa37f08a0..36b82dec4 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -179,6 +179,42 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat 1.0f, "start", "stop", 0.0f, ~0ul); } + if(force && mJumpState != JumpState_None) + { + std::string jump; + MWRender::Animation::Group jumpgroup = MWRender::Animation::Group_All; + if(mJumpState != JumpState_None) + { + jump = "jump"; + if(weap != sWeaponTypeListEnd) + { + jump += weap->shortgroup; + if(!mAnimation->hasAnimation(jump)) + { + jumpgroup = MWRender::Animation::Group_LowerBody; + jump = "jump"; + } + } + } + + if(mJumpState == JumpState_Falling) + { + int mode = ((jump == mCurrentJump) ? 2 : 1); + + mAnimation->disable(mCurrentJump); + mCurrentJump = jump; + mAnimation->play(mCurrentJump, Priority_Jump, jumpgroup, false, + 1.0f, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul); + } + else + { + mAnimation->disable(mCurrentJump); + mCurrentJump.clear(); + mAnimation->play(jump, Priority_Jump, jumpgroup, true, + 1.0f, "loop stop", "stop", 0.0f, 0); + } + } + if(force || movement != mMovementState) { mMovementState = movement; @@ -737,6 +773,7 @@ void CharacterController::update(float duration) { const MWWorld::Store &gmst = world->getStore().get(); + forcestateupdate = (mJumpState != JumpState_Falling); mJumpState = JumpState_Falling; // This is a guess. All that seems to be known is that "While the player is in the @@ -757,6 +794,7 @@ void CharacterController::update(float duration) } else if(vec.z > 0.0f && mJumpState == JumpState_None) { + forcestateupdate = true; mJumpState = JumpState_Falling; float z = cls.getJump(mPtr); @@ -772,6 +810,7 @@ void CharacterController::update(float duration) } else if(mJumpState == JumpState_Falling) { + forcestateupdate = true; mJumpState = JumpState_Landing; vec.z = 0.0f; } @@ -831,7 +870,7 @@ void CharacterController::update(float duration) } if(cls.isNpc()) - forcestateupdate = updateNpcState(onground, inwater, isrunning, sneak); + forcestateupdate = updateNpcState(onground, inwater, isrunning, sneak) || forcestateupdate; refreshCurrentAnims(idlestate, movestate, forcestateupdate); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index ba418eb6a..c943b9597 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -24,6 +24,7 @@ class NpcStats; enum Priority { Priority_Default, + Priority_Jump, Priority_Movement, Priority_Weapon, Priority_Torch, @@ -142,6 +143,7 @@ class CharacterController UpperBodyCharacterState mUpperBodyState; JumpingState mJumpState; + std::string mCurrentJump; WeaponType mWeaponType; std::string mCurrentWeapon; From 2ec39f36222800c33c91986fc6b9d4844cb475cd Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 08:24:47 -0700 Subject: [PATCH 029/194] Don't start the jump animation until after the actor is airborn A bit counter-intuitive, but otherwise certain jump animations will improperly add an offset to the initial inertia. --- apps/openmw/mwmechanics/character.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 36b82dec4..14cc67486 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -794,9 +794,6 @@ void CharacterController::update(float duration) } else if(vec.z > 0.0f && mJumpState == JumpState_None) { - forcestateupdate = true; - mJumpState = JumpState_Falling; - float z = cls.getJump(mPtr); if(vec.x == 0 && vec.y == 0) vec = Ogre::Vector3(0.0f, 0.0f, z); From dca599b8c5435487cb614c8ed8172caf4beeb259 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 08:58:50 -0700 Subject: [PATCH 030/194] Add NPC landing sounds for soundgen keys --- apps/openmw/mwclass/npc.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 620bfaaca..5c46ee75e 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1021,6 +1021,16 @@ namespace MWClass } return ""; } + if(name == "land") + { + MWBase::World *world = MWBase::Environment::get().getWorld(); + Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); + if(world->isUnderwater(ptr.getCell(), pos)) + return "DefaultLandWater"; + if(world->isOnGround(ptr)) + return "Body Fall Medium"; + return ""; + } if(name == "swimleft") return "Swim Left"; if(name == "swimright") @@ -1034,8 +1044,6 @@ namespace MWClass return ""; if(name == "scream") return ""; - if(name == "land") - return ""; throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); } From cf6e3ab9330b60f35a41a4021864b7b489a6fbe5 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Mon, 19 Aug 2013 09:36:51 -0700 Subject: [PATCH 031/194] Fix a potential divide-by-zero --- apps/openmw/mwmechanics/character.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 14cc67486..ec2bb1b59 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -885,7 +885,12 @@ void CharacterController::update(float duration) if(mAnimation && !mSkipAnim) { - Ogre::Vector3 moved = mAnimation->runAnimation(duration) / duration; + Ogre::Vector3 moved = mAnimation->runAnimation(duration); + if(duration > 0.0f) + moved /= duration; + else + moved = Ogre::Vector3(0.0f); + // Ensure we're moving in generally the right direction if(mMovementSpeed > 0.f) { @@ -899,6 +904,7 @@ void CharacterController::update(float duration) (movement.z > 0.0f && movement.z > moved.z*2.0f)) moved.z = movement.z; } + // Update movement if(moved.squaredLength() > 1.0f) world->queueMovement(mPtr, moved); } From a41a23c90afaa42b03362455073bdbdbd9090c32 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 8 Aug 2013 03:10:25 +0200 Subject: [PATCH 032/194] Deleted old code --- components/terrain/esm_land_factory.cpp | 14 --- components/terrain/esm_land_factory.hpp | 37 ------- components/terrain/heightmap.hpp | 38 ------- components/terrain/heightmapbuf.hpp | 72 ------------ components/terrain/land_factory.hpp | 43 -------- components/terrain/tests/.gitignore | 1 - components/terrain/tests/Makefile | 14 --- components/terrain/tests/esm_test.cpp | 10 -- components/terrain/tests/output/esm_test.out | 1 - .../terrain/tests/output/triangle_test.out | 55 --------- components/terrain/tests/test.sh | 18 --- components/terrain/tests/triangle_test.cpp | 93 ---------------- components/terrain/triangulator.hpp | 104 ------------------ 13 files changed, 500 deletions(-) delete mode 100644 components/terrain/esm_land_factory.cpp delete mode 100644 components/terrain/esm_land_factory.hpp delete mode 100644 components/terrain/heightmap.hpp delete mode 100644 components/terrain/heightmapbuf.hpp delete mode 100644 components/terrain/land_factory.hpp delete mode 100644 components/terrain/tests/.gitignore delete mode 100644 components/terrain/tests/Makefile delete mode 100644 components/terrain/tests/esm_test.cpp delete mode 100644 components/terrain/tests/output/esm_test.out delete mode 100644 components/terrain/tests/output/triangle_test.out delete mode 100755 components/terrain/tests/test.sh delete mode 100644 components/terrain/tests/triangle_test.cpp delete mode 100644 components/terrain/triangulator.hpp diff --git a/components/terrain/esm_land_factory.cpp b/components/terrain/esm_land_factory.cpp deleted file mode 100644 index 5cab7ed5d..000000000 --- a/components/terrain/esm_land_factory.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "esm_land_factory.hpp" - -// The first one already includes the others implicitly, but it -// doesn't hurt to be explicit. -#include "../esm_store/store.hpp" -#include "../esm/esmreader.hpp" -#include "../esm/loadland.hpp" - -using namespace Terrain; - -bool ESMLandFactory::has(int x, int y) -{ - return store.landscapes.has(x,y); -} diff --git a/components/terrain/esm_land_factory.hpp b/components/terrain/esm_land_factory.hpp deleted file mode 100644 index bb1f9a8c6..000000000 --- a/components/terrain/esm_land_factory.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef TERRAIN_ESM_LAND_FACTORY_H -#define TERRAIN_ESM_LAND_FACTORY_H - -#include "land_factory.hpp" - -namespace ESMS -{ - struct ESMStore; -} - -namespace ESM -{ - class ESMReader; -} - -namespace Terrain -{ - /* - Land factory that loads data from ESM files. - */ - class ESMLandFactory - { - ESMS::ESMStore &store; - ESM::ESMReader &reader; - - public: - // Initialize the land factory. Note that refrences to the given - // store and reader are stored in the class, so the given objects - // must be valid for a long as you plan to use this factory. - ESMLandFactory(ESMS::ESMStore &st, ESM::ESMReader &rd) - : store(st), reader(rd) {} - - // True if this factory has any data for the given grid cell. - bool has(int x, int y); - }; -} -#endif diff --git a/components/terrain/heightmap.hpp b/components/terrain/heightmap.hpp deleted file mode 100644 index e395b541e..000000000 --- a/components/terrain/heightmap.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef TERRAIN_HEIGHTMAP_H -#define TERRAIN_HEIGHTMAP_H - -/* - Generic interface for a structure holding heightmap data. - - A HeightMap returns information about landscape data in the form of - a regular grid of float heights. - */ - -namespace Terrain -{ - struct HeightMap - { - // Get height from grid position, counted from 0 to getNumX/Y(). - virtual float getHeight(int x, int y) = 0; - - // Get heigth from vertex index, assumed to be y*getNumX() + x. - virtual float getHeight(int index) = 0; - - virtual float getMinX() = 0; - virtual float getMaxX() = 0; - virtual float getMinY() = 0; - virtual float getMaxY() = 0; - - virtual int getNumX() = 0; - virtual int getNumY() = 0; - - // True if the given coordinate is within the grid - bool isWithin(float x, float y) - { - return - x >= getMinX() && x < getMaxX() && - y >= getMinY() && y < getMaxY(); - } - }; -} -#endif diff --git a/components/terrain/heightmapbuf.hpp b/components/terrain/heightmapbuf.hpp deleted file mode 100644 index d147e6015..000000000 --- a/components/terrain/heightmapbuf.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef TERRAIN_HEIGHTMAPBUF_H -#define TERRAIN_HEIGHTMAPBUF_H - -/* - A HeightMap implementation that stores heigths in a buffer. - */ - -#include "heightmap.hpp" -#include "land_factory.hpp" -#include -#include - -namespace Terrain -{ - class HeightMapBuffer : public HeightMap - { - std::vector buf; - - float beginX, sizeX, endX; - float beginY, sizeY, endY; - - int numX, numY; - - public: - void load(LandDataPtr data, const LandInfo &info) - { - // We don't support other kinds of grid data yet. - assert(info.grid == LGT_Quadratic); - assert(info.data == LDT_Float); - - // Set up internal data - beginX = info.xoffset; - sizeX = info.xsize; - endX = beginX+sizeX; - numX = info.numx; - - beginY = info.yoffset; - sizeY = info.ysize; - endY = beginY+sizeY; - numY = info.numy; - - // Prepare the buffer and load it - buf.resize(numX*numY); - - data.read(&buf[0], buf.size()*sizeof(float)); - } - - // Functions inherited from HeightMap: - - float getHeight(int x, int y) - { - assert(x>=0 && x=0 && y= 0 && index < buf.size()); - return buf[index]; - } - - float getMinX() { return beginX; } - float getMaxX() { return endX; } - float getMinY() { return beginY; } - float getMaxY() { return endY; } - - int getNumX() { return numX; } - int getNumY() { return numY; } - }; -} -#endif diff --git a/components/terrain/land_factory.hpp b/components/terrain/land_factory.hpp deleted file mode 100644 index c4d160443..000000000 --- a/components/terrain/land_factory.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef TERRAIN_LAND_FACTORY_H -#define TERRAIN_LAND_FACTORY_H - -namespace Terrain -{ - enum LandInfoGridType - { - LGT_Quadratic - }; - - enum LandInfoDataType - { - LDT_Float - }; - - struct LandInfo - { - // Type information - LandInfoGridType grid; - LandInfoDataType data; - - // Landscape size and number of vertices. Note that xsize and - // ysize may be negative, signaling a flipped landscape in that - // direction. - float xsize, ysize; - int numx, numy; - - // World offset along the same x/y axes. Whether these are set or - // used depends on the client implementation. - float xoffset, yoffset; - }; - - /* - Factory class that provides streams to land data cells. Each - "cell" has a unique integer coordinate in the plane. - */ - struct LandFactory - { - // True if this factory has any data for the given grid cell. - virtual bool has(int x, int y) = 0; - }; -} -#endif diff --git a/components/terrain/tests/.gitignore b/components/terrain/tests/.gitignore deleted file mode 100644 index 814490404..000000000 --- a/components/terrain/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*_test diff --git a/components/terrain/tests/Makefile b/components/terrain/tests/Makefile deleted file mode 100644 index c886f392f..000000000 --- a/components/terrain/tests/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -GCC=g++ - -all: triangle_test esm_test - -LIB_INC=-I../../../libs/ - -triangle_test: triangle_test.cpp - $(GCC) $^ -o $@ - -esm_test: esm_test.cpp - $(GCC) $^ -o $@ $(LIB_INC) - -clean: - rm *_test diff --git a/components/terrain/tests/esm_test.cpp b/components/terrain/tests/esm_test.cpp deleted file mode 100644 index 509aa8aa9..000000000 --- a/components/terrain/tests/esm_test.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -using namespace std; - -#include "../esm_land_factory.hpp" - -int main() -{ - cout << "under development\n"; - return 0; -} diff --git a/components/terrain/tests/output/esm_test.out b/components/terrain/tests/output/esm_test.out deleted file mode 100644 index c6fec4b4d..000000000 --- a/components/terrain/tests/output/esm_test.out +++ /dev/null @@ -1 +0,0 @@ -under development diff --git a/components/terrain/tests/output/triangle_test.out b/components/terrain/tests/output/triangle_test.out deleted file mode 100644 index 001215043..000000000 --- a/components/terrain/tests/output/triangle_test.out +++ /dev/null @@ -1,55 +0,0 @@ -Cell types: -\ / \ / -/ \ / \ -\ / \ / -/ \ / \ - -Full index list: -0 -6 -5 -0 -1 -6 -1 -2 -6 -6 -2 -7 -2 -8 -7 -2 -3 -8 -3 -4 -8 -8 -4 -9 -5 -6 -10 -10 -6 -11 -6 -12 -11 -6 -7 -12 -7 -8 -12 -12 -8 -13 -8 -14 -13 -8 -9 -14 diff --git a/components/terrain/tests/test.sh b/components/terrain/tests/test.sh deleted file mode 100755 index 2d07708ad..000000000 --- a/components/terrain/tests/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -make || exit - -mkdir -p output - -PROGS=*_test - -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done diff --git a/components/terrain/tests/triangle_test.cpp b/components/terrain/tests/triangle_test.cpp deleted file mode 100644 index 464bc8709..000000000 --- a/components/terrain/tests/triangle_test.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include -using namespace std; - -#include "../triangulator.hpp" - -const int X = 4; -const int Y = 4; - -typedef Terrain::Triangulator Triangles4x4; - -int main() -{ - Triangles4x4 t; - - cout << "Cell types:\n"; - for(int y=0;y= 0 && trinum < TriNum); - trinum *= 3; - - p1 = array[trinum++]; - p2 = array[trinum++]; - p3 = array[trinum]; - } - - /* - Get height interpolation weights for a given grid square. The - input is the grid square number (x,y) and the relative position - within that square (xrel,yrel = [0.0..1.0].) The weights are - returned as three vertex index + weight factor pairs. - - A more user-friendly version for HeightMap structs is given - below. - * / - void getWeights(int x, int y, float xrel, float yrel, - Index &p1, float w1, - Index &p2, float w2, - Index &p3, float w3) - { - // Find cell index - int index = y*SizeX + x; - - // First triangle in cell - index *= 2; - - // The rest depends on how the cell is triangulated - if(cellType(x,y)) - { - } - else - { - // Cell is divided as \ from 0,0 to 1,1 - if(xrel < yrel) - { - // Bottom left triangle. - - // Order is (0,0),(1,1),(0,1). - getTriangle(index, p1,p2,p3); - - - } - else - { - // Top right triangle - - // Order is (0,0),(1,0),(1,1). - getTriangle(index+1, p1,p2,p3); - } - } - } - - */ diff --git a/components/terrain/triangulator.hpp b/components/terrain/triangulator.hpp deleted file mode 100644 index c5c0e699b..000000000 --- a/components/terrain/triangulator.hpp +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef TERRAIN_TRIANGULATOR_H -#define TERRAIN_TRIANGULATOR_H - -/* - The triangulator is a simple math helper class, used for dividing a - regular square grid into alternating set of triangles. It divides a - grid like this: - - +----+----+ - | | | - | | | - +----+----+ - | | | - | | | - +----+----+ - - into this: - - +----+----+ - | \ 2|3 / | - |1 \ | / 4| - +----+----+ - |5 / | \ 8| - | / 6|7 \ | - +----+----+ - - Since the triangulation information is typically the same for all - terrains of the same size, once instance can usually be shared. -*/ - -#include - -namespace Terrain -{ - // Index number type, number of grid cells (not vertices) in X and Y - // directions. - template - class Triangulator - { - // Number of triangles - static const int TriNum = SizeX * SizeY * 2; - - // 3 indices per triangle - Index array[TriNum * 3]; - - public: - // Get raw triangle data pointer. Typically used for creating - // meshes. - const Index *getData() { return array; } - - // Return whether a given cell is divided as / (true) or \ - // (false). - static bool cellType(int x, int y) - { - assert(x >= 0 && x < SizeX); - assert(y >= 0 && y < SizeY); - - bool even = (x & 1) == 1; - if((y & 1) == 1) even = !even; - return even; - } - - // Constructor sets up the index buffer - Triangulator() - { - int index = 0; - for ( int y = 0; y < SizeX; y++ ) - for ( int x = 0; x < SizeY; x++ ) - { - // Get vertex indices - Index line1 = y*(SizeX+1) + x; - Index line2 = line1 + SizeX+1; - - if(cellType(x,y)) - { - // Top left - array[index++] = line1; - array[index++] = line1 + 1; - array[index++] = line2; - - // Bottom right - array[index++] = line2; - array[index++] = line1 + 1; - array[index++] = line2 + 1; - } - else - { - // Bottom left - array[index++] = line1; - array[index++] = line2 + 1; - array[index++] = line2; - - // Top right - array[index++] = line1; - array[index++] = line1 + 1; - array[index++] = line2 + 1; - } - } - assert(index == TriNum*3); - } - }; -} // Namespace - -#endif From e27437f8ede5da7d7e58280f8f96559171e654ef Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 16 Aug 2013 13:01:52 +0200 Subject: [PATCH 033/194] New terrain renderer - improvements: - Consistent triangle alignment, fixes a noticable crack near the census and excise office. Note that alignment is still not the same as vanilla. Vanilla uses a weird diagonal pattern. I hope there aren't more trouble spots that will force us to replicate vanilla, but at least we can do that now. - Fixes several blending issues and cell border seams - Fix map render to use the terrain bounding box instead of an arbitrary height - Different LODs are now properly connected instead of using skirts - Support self shadowing - Normals and colors are stored in the vertices instead of a texture, this enables per-vertex lighting which should improve performance, fix compatibility issues due to the PS getting too large and mimic vanilla better - Support a fixed function fallback (though the splatting shader usually performs better) - Designed for distant land support - test: https://www.youtube.com/watch?v=2wnd9EuPJIY - we can't really enable this yet due to depth precision issues when using a large view distance --- apps/openmw/CMakeLists.txt | 3 +- apps/openmw/engine.cpp | 6 +- apps/openmw/mwrender/globalmap.cpp | 11 +- apps/openmw/mwrender/localmap.cpp | 8 +- apps/openmw/mwrender/localmap.hpp | 10 +- apps/openmw/mwrender/renderingmanager.cpp | 37 +- apps/openmw/mwrender/renderingmanager.hpp | 8 +- apps/openmw/mwrender/shadows.cpp | 1 - apps/openmw/mwrender/terrain.cpp | 531 ---------------------- apps/openmw/mwrender/terrain.hpp | 128 ------ apps/openmw/mwrender/terrainmaterial.cpp | 246 ---------- apps/openmw/mwrender/terrainmaterial.hpp | 88 ---- apps/openmw/mwrender/terrainstorage.cpp | 56 +++ apps/openmw/mwrender/terrainstorage.hpp | 22 + apps/openmw/mwrender/water.cpp | 5 +- apps/openmw/mwworld/scene.cpp | 4 +- apps/openmw/mwworld/worldimp.cpp | 6 +- components/CMakeLists.txt | 4 + components/esm/loadland.hpp | 2 +- components/terrain/chunk.cpp | 171 +++++++ components/terrain/chunk.hpp | 63 +++ components/terrain/material.cpp | 291 ++++++++++++ components/terrain/material.hpp | 54 +++ components/terrain/quadtreenode.cpp | 387 ++++++++++++++++ components/terrain/quadtreenode.hpp | 141 ++++++ components/terrain/storage.cpp | 318 +++++++++++++ components/terrain/storage.hpp | 76 ++++ components/terrain/terrain.cpp | 359 +++++++++++++++ components/terrain/terrain.hpp | 121 +++++ files/materials/objects.shader | 1 - files/materials/terrain.shader | 162 ++++--- libs/openengine/ogre/imagerotate.cpp | 4 +- 32 files changed, 2203 insertions(+), 1121 deletions(-) delete mode 100644 apps/openmw/mwrender/terrain.cpp delete mode 100644 apps/openmw/mwrender/terrain.hpp delete mode 100644 apps/openmw/mwrender/terrainmaterial.cpp delete mode 100644 apps/openmw/mwrender/terrainmaterial.hpp create mode 100644 apps/openmw/mwrender/terrainstorage.cpp create mode 100644 apps/openmw/mwrender/terrainstorage.hpp create mode 100644 components/terrain/chunk.cpp create mode 100644 components/terrain/chunk.hpp create mode 100644 components/terrain/material.cpp create mode 100644 components/terrain/material.hpp create mode 100644 components/terrain/quadtreenode.cpp create mode 100644 components/terrain/quadtreenode.hpp create mode 100644 components/terrain/storage.cpp create mode 100644 components/terrain/storage.hpp create mode 100644 components/terrain/terrain.cpp create mode 100644 components/terrain/terrain.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index eebecc09c..8259ac8cf 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -15,8 +15,9 @@ source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender renderingmanager debugging sky camera animation npcanimation creatureanimation activatoranimation - actors objects renderinginterface localmap occlusionquery terrain terrainmaterial water shadows + actors objects renderinginterface localmap occlusionquery water shadows compositors characterpreview externalrendering globalmap videoplayer ripplesimulation refraction + terrainstorage ) add_openmw_dir (mwinput diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 9516da5ae..23e94cb51 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -91,12 +91,12 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::Environment::get().getSoundManager()->update(frametime); // global scripts - MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + //MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); bool changed = MWBase::Environment::get().getWorld()->hasCellChanged(); // local scripts - executeLocalScripts(); // This does not handle the case where a global script causes a cell + //executeLocalScripts(); // This does not handle the case where a global script causes a cell // change, followed by a cell change in a local script during the same // frame. @@ -597,4 +597,4 @@ void OMW::Engine::setStartupScript (const std::string& path) void OMW::Engine::setActivationDistanceOverride (int distance) { mActivationDistanceOverride = distance; -} \ No newline at end of file +} diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 1ff99dda8..78b13ec07 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -58,8 +58,6 @@ namespace MWRender //if (!boost::filesystem::exists(mCacheDir + "/GlobalMap.png")) if (1) { - Ogre::Image image; - std::vector data (mWidth * mHeight * 3); for (int x = mMinX; x <= mMaxX; ++x) @@ -144,9 +142,6 @@ namespace MWRender b = waterDeepColour.b * 255; } - // uncomment this line to outline cell borders - //if (cellX == 0 || cellX == cellSize-1 || cellY == 0|| cellY == cellSize-1) r = 255; - data[texelY * mWidth * 3 + texelX * 3] = r; data[texelY * mWidth * 3 + texelX * 3+1] = g; data[texelY * mWidth * 3 + texelX * 3+2] = b; @@ -155,13 +150,11 @@ namespace MWRender } } - image.loadDynamicImage (&data[0], mWidth, mHeight, Ogre::PF_B8G8R8); - - //image.save (mCacheDir + "/GlobalMap.png"); + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); tex = Ogre::TextureManager::getSingleton ().createManual ("GlobalMap.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_B8G8R8, Ogre::TU_STATIC); - tex->loadImage(image); + tex->loadRawData(stream, mWidth, mHeight, Ogre::PF_B8G8R8); } else tex = Ogre::TextureManager::getSingleton ().getByName ("GlobalMap.png"); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 8043f8b12..c41484452 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -2,6 +2,10 @@ #include #include +#include +#include +#include +#include #include "../mwworld/esmstore.hpp" @@ -104,7 +108,7 @@ void LocalMap::saveFogOfWar(MWWorld::Ptr::CellStore* cell) } } -void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell) +void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell, float zMin, float zMax) { mInterior = false; @@ -118,7 +122,7 @@ void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell) mCameraPosNode->setPosition(Vector3(0,0,0)); - render((x+0.5)*sSize, (y+0.5)*sSize, -10000, 10000, sSize, sSize, name); + render((x+0.5)*sSize, (y+0.5)*sSize, zMin, zMax, sSize, sSize, name); } void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell, diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 72e637d9a..538489640 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -28,16 +28,18 @@ namespace MWRender * Request the local map for an exterior cell. * @remarks It will either be loaded from a disk cache, * or rendered if it is not already cached. - * @param exterior cell + * @param cell exterior cell + * @param zMin min height of objects or terrain in cell + * @param zMax max height of objects or terrain in cell */ - void requestMap (MWWorld::CellStore* cell); + void requestMap (MWWorld::CellStore* cell, float zMin, float zMax); /** * Request the local map for an interior cell. * @remarks It will either be loaded from a disk cache, * or rendered if it is not already cached. - * @param interior cell - * @param bounding box of the cell + * @param cell interior cell + * @param bounds bounding box of the cell */ void requestMap (MWWorld::CellStore* cell, Ogre::AxisAlignedBox bounds); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index f3e518800..9f7fde10a 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -25,6 +25,8 @@ #include #include +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -46,6 +48,7 @@ #include "externalrendering.hpp" #include "globalmap.hpp" #include "videoplayer.hpp" +#include "terrainstorage.hpp" using namespace MWRender; using namespace Ogre; @@ -63,6 +66,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b , mAmbientMode(0) , mSunEnabled(0) , mPhysicsEngine(engine) + , mTerrain(NULL) { // select best shader mode bool openGL = (Ogre::Root::getSingleton ().getRenderSystem ()->getName().find("OpenGL") != std::string::npos); @@ -78,7 +82,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b Settings::Manager::setString("shader mode", "General", openGL ? (glES ? "glsles" : "glsl") : "hlsl"); } - mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 5); + mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 50); mRendering.getWindow()->addListener(this); mRendering.setWindowListener(this); @@ -166,8 +170,6 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b mShadows = new Shadows(&mRendering); - mTerrainManager = new TerrainManager(mRendering.getScene(), this); - mSkyManager = new SkyManager(mRootNode, mRendering.getCamera()); mOcclusionQuery = new OcclusionQuery(&mRendering, mSkyManager->getSunNode()); @@ -194,7 +196,7 @@ RenderingManager::~RenderingManager () delete mSkyManager; delete mDebugging; delete mShadows; - delete mTerrainManager; + delete mTerrain; delete mLocalMap; delete mOcclusionQuery; delete mCompositors; @@ -225,8 +227,6 @@ void RenderingManager::removeCell (MWWorld::Ptr::CellStore *store) mObjects.removeCell(store); mActors.removeCell(store); mDebugging->cellRemoved(store); - if (store->mCell->isExterior()) - mTerrainManager->cellRemoved(store); } void RenderingManager::removeWater () @@ -244,8 +244,14 @@ void RenderingManager::cellAdded (MWWorld::Ptr::CellStore *store) mObjects.buildStaticGeometry (*store); sh::Factory::getInstance().unloadUnreferencedMaterials(); mDebugging->cellAdded(store); - if (store->mCell->isExterior()) - mTerrainManager->cellAdded(store); + if (store->isExterior()) + { + if (!mTerrain) + { + mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain); + mTerrain->update(mRendering.getCamera()); + } + } waterAdded(store); } @@ -550,7 +556,8 @@ void RenderingManager::setAmbientMode() float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) { - return mTerrainManager->getTerrainHeightAt(worldPos); + assert(mTerrain); + return mTerrain->getHeightAt(worldPos); } @@ -595,7 +602,6 @@ void RenderingManager::setSunColour(const Ogre::ColourValue& colour) if (!mSunEnabled) return; mSun->setDiffuseColour(colour); mSun->setSpecularColour(colour); - mTerrainManager->setDiffuse(colour); } void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) @@ -608,7 +614,6 @@ void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) final += Ogre::ColourValue(0.7,0.7,0.7,0) * std::min(1.f, (nightEye/100.f)); mRendering.getScene()->setAmbientLight(final); - mTerrainManager->setAmbient(final); } void RenderingManager::sunEnable(bool real) @@ -652,7 +657,12 @@ void RenderingManager::setGlare(bool glare) void RenderingManager::requestMap(MWWorld::Ptr::CellStore* cell) { if (cell->mCell->isExterior()) - mLocalMap->requestMap(cell); + { + Ogre::AxisAlignedBox dims = mObjects.getDimensions(cell); + Ogre::Vector2 center(cell->mCell->getGridX() + 0.5, -cell->mCell->getGridY() + 1 - 0.5); + dims.merge(mTerrain->getWorldBoundingBox(center)); + mLocalMap->requestMap(cell, dims.getMinimum().z, dims.getMaximum().z); + } else mLocalMap->requestMap(cell, mObjects.getDimensions(cell)); } @@ -984,6 +994,9 @@ void RenderingManager::updateWaterRippleEmitterPtr (const MWWorld::Ptr& old, con void RenderingManager::frameStarted(float dt) { + if (mTerrain) + mTerrain->update(mRendering.getCamera()); + mWater->frameStarted(dt); } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 17cdfff4a..c4fb05804 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -2,7 +2,6 @@ #define _GAME_RENDERING_MANAGER_H #include "sky.hpp" -#include "terrain.hpp" #include "debugging.hpp" #include @@ -39,6 +38,11 @@ namespace sh class Factory; } +namespace Terrain +{ + class Terrain; +} + namespace MWRender { class Shadows; @@ -228,7 +232,7 @@ private: OcclusionQuery* mOcclusionQuery; - TerrainManager* mTerrainManager; + Terrain::Terrain* mTerrain; MWRender::Water *mWater; diff --git a/apps/openmw/mwrender/shadows.cpp b/apps/openmw/mwrender/shadows.cpp index 0d066a0ec..c28c01dcc 100644 --- a/apps/openmw/mwrender/shadows.cpp +++ b/apps/openmw/mwrender/shadows.cpp @@ -108,7 +108,6 @@ void Shadows::recreate() int visibilityMask = RV_Actors * Settings::Manager::getBool("actor shadows", "Shadows") + (RV_Statics + RV_StaticsSmall) * Settings::Manager::getBool("statics shadows", "Shadows") + RV_Misc * Settings::Manager::getBool("misc shadows", "Shadows"); - for (int i = 0; i < (split ? 3 : 1); ++i) { TexturePtr shadowTexture = mSceneMgr->getShadowTexture(i); diff --git a/apps/openmw/mwrender/terrain.cpp b/apps/openmw/mwrender/terrain.cpp deleted file mode 100644 index 1b829a2c2..000000000 --- a/apps/openmw/mwrender/terrain.cpp +++ /dev/null @@ -1,531 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "../mwworld/esmstore.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "terrainmaterial.hpp" -#include "terrain.hpp" -#include "renderconst.hpp" -#include "shadows.hpp" -#include "renderingmanager.hpp" - -using namespace Ogre; - -namespace MWRender -{ - - //---------------------------------------------------------------------------------------------- - - TerrainManager::TerrainManager(Ogre::SceneManager* mgr, RenderingManager* rend) : - mTerrainGroup(TerrainGroup(mgr, Terrain::ALIGN_X_Y, mLandSize, mWorldSize)), mRendering(rend) - { - mTerrainGlobals = OGRE_NEW TerrainGlobalOptions(); - - TerrainMaterialGeneratorPtr matGen; - TerrainMaterial* matGenP = new TerrainMaterial(); - matGen.bind(matGenP); - mTerrainGlobals->setDefaultMaterialGenerator(matGen); - - TerrainMaterialGenerator::Profile* const activeProfile = - mTerrainGlobals->getDefaultMaterialGenerator() - ->getActiveProfile(); - mActiveProfile = static_cast(activeProfile); - - // We don't want any pixel error at all. Really, LOD makes no sense here - morrowind uses 65x65 verts in one cell, - // so applying LOD is most certainly slower than doing no LOD at all. - // Setting this to 0 seems to cause glitches though. :/ - mTerrainGlobals->setMaxPixelError(1); - - mTerrainGlobals->setLayerBlendMapSize(ESM::Land::LAND_TEXTURE_SIZE/2 + 1); - - //10 (default) didn't seem to be quite enough - mTerrainGlobals->setSkirtSize(128); - - //due to the sudden flick between composite and non composite textures, - //this seemed the distance where it wasn't too noticeable - mTerrainGlobals->setCompositeMapDistance(mWorldSize*2); - - mTerrainGroup.setOrigin(Vector3(mWorldSize/2, - mWorldSize/2, - 0)); - - Terrain::ImportData& importSettings = mTerrainGroup.getDefaultImportSettings(); - - importSettings.inputBias = 0; - importSettings.terrainSize = mLandSize; - importSettings.worldSize = mWorldSize; - importSettings.minBatchSize = 9; - importSettings.maxBatchSize = mLandSize; - - importSettings.deleteInputData = true; - } - - //---------------------------------------------------------------------------------------------- - - float TerrainManager::getTerrainHeightAt(Vector3 worldPos) - { - Ogre::Terrain* terrain = NULL; - float height = mTerrainGroup.getHeightAtWorldPosition(worldPos, &terrain); - if (terrain == NULL) - return std::numeric_limits().min(); - return height; - } - - //---------------------------------------------------------------------------------------------- - - TerrainManager::~TerrainManager() - { - OGRE_DELETE mTerrainGlobals; - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::setDiffuse(const ColourValue& diffuse) - { - mTerrainGlobals->setCompositeMapDiffuse(diffuse); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::setAmbient(const ColourValue& ambient) - { - mTerrainGlobals->setCompositeMapAmbient(ambient); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::cellAdded(MWWorld::Ptr::CellStore *store) - { - const int cellX = store->mCell->getGridX(); - const int cellY = store->mCell->getGridY(); - - ESM::Land* land = - MWBase::Environment::get().getWorld()->getStore().get().search(cellX, cellY); - if (land == NULL) // no land data means we're not going to create any terrain. - return; - - int dataRequired = ESM::Land::DATA_VHGT | ESM::Land::DATA_VCLR; - if (!land->isDataLoaded(dataRequired)) - { - land->loadData(dataRequired); - } - - //split the cell terrain into four segments - const int numTextures = ESM::Land::LAND_TEXTURE_SIZE/2; - - for ( int x = 0; x < 2; x++ ) - { - for ( int y = 0; y < 2; y++ ) - { - Terrain::ImportData terrainData = - mTerrainGroup.getDefaultImportSettings(); - - const int terrainX = cellX * 2 + x; - const int terrainY = cellY * 2 + y; - - //it makes far more sense to reallocate the memory here, - //and let Ogre deal with it due to the issues with deleting - //it at the wrong time if using threads (Which Terrain does) - terrainData.inputFloat = OGRE_ALLOC_T(float, - mLandSize*mLandSize, - MEMCATEGORY_GEOMETRY); - - //copy the height data row by row - for ( int terrainCopyY = 0; terrainCopyY < mLandSize; terrainCopyY++ ) - { - //the offset of the current segment - const size_t yOffset = y * (mLandSize-1) * ESM::Land::LAND_SIZE + - //offset of the row - terrainCopyY * ESM::Land::LAND_SIZE; - const size_t xOffset = x * (mLandSize-1); - - memcpy(&terrainData.inputFloat[terrainCopyY*mLandSize], - &land->mLandData->mHeights[yOffset + xOffset], - mLandSize*sizeof(float)); - } - - std::map indexes; - initTerrainTextures(&terrainData, cellX, cellY, - x * numTextures, y * numTextures, - numTextures, indexes, land->mPlugin); - - if (mTerrainGroup.getTerrain(terrainX, terrainY) == NULL) - { - mTerrainGroup.defineTerrain(terrainX, terrainY, &terrainData); - - mTerrainGroup.loadTerrain(terrainX, terrainY, true); - - Terrain* terrain = mTerrainGroup.getTerrain(terrainX, terrainY); - initTerrainBlendMaps(terrain, - cellX, cellY, - x * numTextures, y * numTextures, - numTextures, - indexes); - terrain->setVisibilityFlags(RV_Terrain); - terrain->setRenderQueueGroup(RQG_Main); - - // disable or enable global colour map (depends on available vertex colours) - if ( land->mLandData->mUsingColours ) - { - TexturePtr vertex = getVertexColours(land, - cellX, cellY, - x*(mLandSize-1), - y*(mLandSize-1), - mLandSize); - - mActiveProfile->setGlobalColourMapEnabled(true); - mActiveProfile->setGlobalColourMap (terrain, vertex->getName()); - } - else - mActiveProfile->setGlobalColourMapEnabled (false); - } - } - } - - // when loading from a heightmap, Ogre::Terrain does not update the derived data (normal map, LOD) - // synchronously, even if we supply synchronous = true parameter to loadTerrain. - // the following to be the only way to make sure derived data is ready when rendering the next frame. - while (mTerrainGroup.isDerivedDataUpdateInProgress()) - { - // we need to wait for this to finish - OGRE_THREAD_SLEEP(5); - Root::getSingleton().getWorkQueue()->processResponses(); - } - - mTerrainGroup.freeTemporaryResources(); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::cellRemoved(MWWorld::Ptr::CellStore *store) - { - for ( int x = 0; x < 2; x++ ) - { - for ( int y = 0; y < 2; y++ ) - { - int terrainX = store->mCell->getGridX() * 2 + x; - int terrainY = store->mCell->getGridY() * 2 + y; - if (mTerrainGroup.getTerrain(terrainX, terrainY) != NULL) - mTerrainGroup.unloadTerrain(terrainX, terrainY); - } - } - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData, - int cellX, int cellY, - int fromX, int fromY, int size, - std::map& indexes, size_t plugin) - { - // FIXME: In a multiple esm configuration, we have multiple palettes. Since this code - // crosses cell boundaries, we no longer have a unique terrain palette. Instead, we need - // to adopt the following code for a dynamic palette. And this is evil - the current design - // does not work well for this task... - - assert(terrainData != NULL && "Must have valid terrain data"); - assert(fromX >= 0 && fromY >= 0 && - "Can't get a terrain texture on terrain outside the current cell"); - assert(fromX+size <= ESM::Land::LAND_TEXTURE_SIZE && - fromY+size <= ESM::Land::LAND_TEXTURE_SIZE && - "Can't get a terrain texture on terrain outside the current cell"); - - //this ensures that the ltex indexes are sorted (or retrived as sorted - //which simplifies shading between cells). - // - //If we don't sort the ltex indexes, the splatting order may differ between - //cells which may lead to inconsistent results when shading between cells - int num = MWBase::Environment::get().getWorld()->getStore().get().getSize(plugin); - std::set ltexIndexes; - for ( int y = fromY; y < fromY + size + 1; y++ ) - { - for ( int x = fromX - 1; x < fromX + size; x++ ) // NB we wrap X from the other side because Y is reversed - { - int idx = getLtexIndexAt(cellX, cellY, x, y); - // This is a quick hack to prevent the program from trying to fetch textures - // from a neighboring cell, which might originate from a different plugin, - // and use a separate texture palette. Right now, we simply cast it to the - // default texture (i.e. 0). - if (idx > num) - idx = 0; - ltexIndexes.insert(idx); - } - } - - //there is one texture that we want to use as a base (i.e. it won't have - //a blend map). This holds the ltex index of that base texture so that - //we know not to include it in the output map - int baseTexture = -1; - for ( std::set::iterator iter = ltexIndexes.begin(); - iter != ltexIndexes.end(); - ++iter ) - { - uint16_t ltexIndex = *iter; - //this is the base texture, so we can ignore this at present - if ( ltexIndex == baseTexture ) - { - continue; - } - - const std::map::const_iterator it = indexes.find(ltexIndex); - - if ( it == indexes.end() ) - { - //NB: All vtex ids are +1 compared to the ltex ids - - const MWWorld::Store <exStore = - MWBase::Environment::get().getWorld()->getStore().get(); - - // NOTE: using the quick hack above, we should no longer end up with textures indices - // that are out of bounds. However, I haven't updated the test to a multi-palette - // system yet. We probably need more work here, so we skip it for now. - //assert( (int)ltexStore.getSize() >= (int)ltexIndex - 1 && - //"LAND.VTEX must be within the bounds of the LTEX array"); - - std::string texture; - if ( ltexIndex == 0 ) - { - texture = "_land_default.dds"; - } - else - { - texture = ltexStore.search(ltexIndex-1, plugin)->mTexture; - //TODO this is needed due to MWs messed up texture handling - texture = texture.substr(0, texture.rfind(".")) + ".dds"; - } - - const size_t position = terrainData->layerList.size(); - terrainData->layerList.push_back(Terrain::LayerInstance()); - - terrainData->layerList[position].worldSize = 256; - terrainData->layerList[position].textureNames.push_back("textures\\" + texture); - - if ( baseTexture == -1 ) - { - baseTexture = ltexIndex; - } - else - { - indexes[ltexIndex] = position; - } - } - } - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::initTerrainBlendMaps(Terrain* terrain, - int cellX, int cellY, - int fromX, int fromY, int size, - const std::map& indexes) - { - assert(terrain != NULL && "Must have valid terrain"); - assert(fromX >= 0 && fromY >= 0 && - "Can't get a terrain texture on terrain outside the current cell"); - assert(fromX+size <= ESM::Land::LAND_TEXTURE_SIZE && - fromY+size <= ESM::Land::LAND_TEXTURE_SIZE && - "Can't get a terrain texture on terrain outside the current cell"); - - //size must be a power of 2 as we do divisions with a power of 2 number - //that need to result in an integer for correct splatting - assert( (size & (size - 1)) == 0 && "Size must be a power of 2"); - - const int blendMapSize = terrain->getLayerBlendMapSize(); - - //zero out every map - std::map::const_iterator iter; - for ( iter = indexes.begin(); iter != indexes.end(); ++iter ) - { - float* pBlend = terrain->getLayerBlendMap(iter->second) - ->getBlendPointer(); - memset(pBlend, 0, sizeof(float) * blendMapSize * blendMapSize); - } - - //covert the ltex data into a set of blend maps - for ( int texY = fromY; texY < fromY + size + 1; texY++ ) - { - for ( int texX = fromX - 1; texX < fromX + size; texX++ ) // NB we wrap X from the other side because Y is reversed - { - const uint16_t ltexIndex = getLtexIndexAt(cellX, cellY, texX, texY); - - //check if it is the base texture (which isn't in the map) and - //if it is don't bother altering the blend map for it - if ( indexes.find(ltexIndex) == indexes.end() ) - { - continue; - } - - //while texX is the splat index relative to the entire cell, - //relX is relative to the current segment we are splatting - const int relX = texX - fromX + 1; - const int relY = texY - fromY; - - const int layerIndex = indexes.find(ltexIndex)->second; - - float* const pBlend = terrain->getLayerBlendMap(layerIndex) - ->getBlendPointer(); - - //Note: Y is reversed - const int splatY = blendMapSize - relY - 1; - const int splatX = relX; - - assert(splatX >= 0 && splatX < blendMapSize); - assert(splatY >= 0 && splatY < blendMapSize); - - const int index = (splatY)*blendMapSize + splatX; - pBlend[index] = 1; - } - } - - for ( int i = 1; i < terrain->getLayerCount(); i++ ) - { - TerrainLayerBlendMap* blend = terrain->getLayerBlendMap(i); - blend->dirty(); - blend->update(); - } - - } - - //---------------------------------------------------------------------------------------------- - - int TerrainManager::getLtexIndexAt(int cellX, int cellY, - int x, int y) - { - //check texture index falls within the 9 cell bounds - //as this function can't cope with anything above that - assert(x >= -ESM::Land::LAND_TEXTURE_SIZE && - y >= -ESM::Land::LAND_TEXTURE_SIZE && - "Trying to get land textures that are out of bounds"); - - assert(x < 2*ESM::Land::LAND_TEXTURE_SIZE && - y < 2*ESM::Land::LAND_TEXTURE_SIZE && - "Trying to get land textures that are out of bounds"); - - if ( x < 0 ) - { - cellX--; - x += ESM::Land::LAND_TEXTURE_SIZE; - } - else if ( x >= ESM::Land::LAND_TEXTURE_SIZE ) - { - cellX++; - x -= ESM::Land::LAND_TEXTURE_SIZE; - } - - if ( y < 0 ) - { - cellY--; - y += ESM::Land::LAND_TEXTURE_SIZE; - } - else if ( y >= ESM::Land::LAND_TEXTURE_SIZE ) - { - cellY++; - y -= ESM::Land::LAND_TEXTURE_SIZE; - } - - - ESM::Land* land = - MWBase::Environment::get().getWorld()->getStore().get().search(cellX, cellY); - if ( land != NULL ) - { - if (!land->isDataLoaded(ESM::Land::DATA_VTEX)) - { - land->loadData(ESM::Land::DATA_VTEX); - } - - return land->mLandData - ->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; - } - else - { - return 0; - } - } - - //---------------------------------------------------------------------------------------------- - - TexturePtr TerrainManager::getVertexColours(ESM::Land* land, - int cellX, int cellY, - int fromX, int fromY, int size) - { - TextureManager* const texMgr = TextureManager::getSingletonPtr(); - - const std::string colourTextureName = "VtexColours_" + - boost::lexical_cast(cellX) + - "_" + - boost::lexical_cast(cellY) + - "_" + - boost::lexical_cast(fromX) + - "_" + - boost::lexical_cast(fromY); - - TexturePtr tex = texMgr->getByName(colourTextureName); - if ( !tex.isNull() ) - { - return tex; - } - - tex = texMgr->createManual(colourTextureName, - ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - TEX_TYPE_2D, size, size, 0, PF_BYTE_BGR); - - HardwarePixelBufferSharedPtr pixelBuffer = tex->getBuffer(); - - pixelBuffer->lock(HardwareBuffer::HBL_DISCARD); - const PixelBox& pixelBox = pixelBuffer->getCurrentLock(); - - uint8* pDest = static_cast(pixelBox.data); - - if ( land != NULL ) - { - const char* const colours = land->mLandData->mColours; - for ( int y = 0; y < size; y++ ) - { - for ( int x = 0; x < size; x++ ) - { - const size_t colourOffset = (y+fromY)*3*65 + (x+fromX)*3; - - assert( colourOffset < 65*65*3 && - "Colour offset is out of the expected bounds of record" ); - - const unsigned char r = colours[colourOffset + 0]; - const unsigned char g = colours[colourOffset + 1]; - const unsigned char b = colours[colourOffset + 2]; - - //as is the case elsewhere we need to flip the y - const size_t imageOffset = (size - 1 - y)*size*4 + x*4; - pDest[imageOffset + 0] = b; - pDest[imageOffset + 1] = g; - pDest[imageOffset + 2] = r; - } - } - } - else - { - for ( int y = 0; y < size; y++ ) - { - for ( int x = 0; x < size; x++ ) - { - for ( int k = 0; k < 3; k++ ) - { - *pDest++ = 0; - } - } - } - } - - pixelBuffer->unlock(); - - return tex; - } - -} diff --git a/apps/openmw/mwrender/terrain.hpp b/apps/openmw/mwrender/terrain.hpp deleted file mode 100644 index 45c56390e..000000000 --- a/apps/openmw/mwrender/terrain.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#ifndef _GAME_RENDER_TERRAIN_H -#define _GAME_RENDER_TERRAIN_H - -#include -#include - -#include - -#include "terrainmaterial.hpp" - -namespace Ogre{ - class SceneManager; - class TerrainGroup; - class TerrainGlobalOptions; - class Terrain; -} - -namespace MWWorld -{ - class CellStore; -} - -namespace MWRender{ - - class RenderingManager; - - /** - * Implements the Morrowind terrain using the Ogre Terrain Component - * - * Each terrain cell is split into four blocks as this leads to an increase - * in performance and means we don't hit splat limits quite as much - */ - class TerrainManager{ - public: - TerrainManager(Ogre::SceneManager* mgr, RenderingManager* rend); - virtual ~TerrainManager(); - - void setDiffuse(const Ogre::ColourValue& diffuse); - void setAmbient(const Ogre::ColourValue& ambient); - - void cellAdded(MWWorld::CellStore* store); - void cellRemoved(MWWorld::CellStore* store); - - float getTerrainHeightAt (Ogre::Vector3 worldPos); - - private: - Ogre::TerrainGlobalOptions* mTerrainGlobals; - Ogre::TerrainGroup mTerrainGroup; - - RenderingManager* mRendering; - - TerrainMaterial::Profile* mActiveProfile; - - /** - * The length in verticies of a single terrain block. - */ - static const int mLandSize = (ESM::Land::LAND_SIZE - 1)/2 + 1; - - /** - * The length in game units of a single terrain block. - */ - static const int mWorldSize = ESM::Land::REAL_SIZE/2; - - /** - * Setups up the list of textures for part of a cell, using indexes as - * an output to create a mapping of MW LtexIndex to the relevant terrain - * layer - * - * @param terrainData the terrain data to setup the textures for - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the ltex index in the current cell to start making the texture from - * @param fromY the ltex index in the current cell to start making the texture from - * @param size the size (number of splats) to get - * @param indexes a mapping of ltex index to the terrain texture layer that - * can be used by initTerrainBlendMaps - */ - void initTerrainTextures(Ogre::Terrain::ImportData* terrainData, - int cellX, int cellY, - int fromX, int fromY, int size, - std::map& indexes, size_t plugin); - - /** - * Creates the blend (splatting maps) for the given terrain from the ltex data. - * - * @param terrain the terrain object for the current cell - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the ltex index in the current cell to start making the texture from - * @param fromY the ltex index in the current cell to start making the texture from - * @param size the size (number of splats) to get - * @param indexes the mapping of ltex to blend map produced by initTerrainTextures - */ - void initTerrainBlendMaps(Ogre::Terrain* terrain, - int cellX, int cellY, - int fromX, int fromY, int size, - const std::map& indexes); - - /** - * Gets a LTEX index at the given point, assuming the current cell - * starts at (0,0). This supports getting values from the surrounding - * cells so negative x, y is acceptable - * - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param x, y the splat position of the ltex index to get relative to the - * first splat of the current cell - */ - int getLtexIndexAt(int cellX, int cellY, int x, int y); - - /** - * Due to the fact that Ogre terrain doesn't support vertex colours - * we have to generate them manually - * - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the *vertex* index in the current cell to start making texture from - * @param fromY the *vertex* index in the current cell to start making the texture from - * @param size the size (number of vertexes) to get - */ - Ogre::TexturePtr getVertexColours(ESM::Land* land, - int cellX, int cellY, - int fromX, int fromY, int size); - }; - -} - -#endif // _GAME_RENDER_TERRAIN_H diff --git a/apps/openmw/mwrender/terrainmaterial.cpp b/apps/openmw/mwrender/terrainmaterial.cpp deleted file mode 100644 index 892dab7cf..000000000 --- a/apps/openmw/mwrender/terrainmaterial.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include "terrainmaterial.hpp" - -#include - -#include - -#include - -namespace -{ - Ogre::String getComponent (int num) - { - if (num == 0) - return "x"; - else if (num == 1) - return "y"; - else if (num == 2) - return "z"; - else - return "w"; - } -} - - -namespace MWRender -{ - - TerrainMaterial::TerrainMaterial() - { - mLayerDecl.samplers.push_back(Ogre::TerrainLayerSampler("albedo_specular", Ogre::PF_BYTE_RGBA)); - //mLayerDecl.samplers.push_back(Ogre::TerrainLayerSampler("normal_height", Ogre::PF_BYTE_RGBA)); - - mLayerDecl.elements.push_back( - Ogre::TerrainLayerSamplerElement(0, Ogre::TLSS_ALBEDO, 0, 3)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(0, Ogre::TLSS_SPECULAR, 3, 1)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(1, Ogre::TLSS_NORMAL, 0, 3)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(1, Ogre::TLSS_HEIGHT, 3, 1)); - - - mProfiles.push_back(OGRE_NEW Profile(this, "SM2", "Profile for rendering on Shader Model 2 capable cards")); - setActiveProfile("SM2"); - } - - // ----------------------------------------------------------------------------------------------------------------------- - - TerrainMaterial::Profile::Profile(Ogre::TerrainMaterialGenerator* parent, const Ogre::String& name, const Ogre::String& desc) - : Ogre::TerrainMaterialGenerator::Profile(parent, name, desc) - , mGlobalColourMap(false) - , mMaterial(0) - { - } - - TerrainMaterial::Profile::~Profile() - { - if (mMaterial) - sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName()); - } - - Ogre::MaterialPtr TerrainMaterial::Profile::generate(const Ogre::Terrain* terrain) - { - const Ogre::String& matName = terrain->getMaterialName(); - - sh::Factory::getInstance().destroyMaterialInstance (matName); - - Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(matName); - if (!mat.isNull()) - Ogre::MaterialManager::getSingleton().remove(matName); - - mMaterial = sh::Factory::getInstance().createMaterialInstance (matName); - mMaterial->setProperty ("allow_fixed_function", sh::makeProperty(new sh::BooleanValue(false))); - - int numPasses = getRequiredPasses(terrain); - int maxLayersInOnePass = getMaxLayersPerPass(terrain); - - for (int pass=0; passcreatePass (); - - p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); - p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); - if (pass != 0) - { - p->setProperty ("scene_blend", sh::makeProperty(new sh::StringValue("alpha_blend"))); - // Only write if depth is equal to the depth value written by the previous pass. - p->setProperty ("depth_func", sh::makeProperty(new sh::StringValue("equal"))); - } - - p->mShaderProperties.setProperty ("colour_map", sh::makeProperty(new sh::BooleanValue(mGlobalColourMap))); - p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(pass == 0))); - - // global colour map - sh::MaterialInstanceTextureUnit* colourMap = p->createTextureUnit ("colourMap"); - colourMap->setProperty ("texture_alias", sh::makeProperty (new sh::StringValue(mMaterial->getName() + "_colourMap"))); - colourMap->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - - // global normal map - sh::MaterialInstanceTextureUnit* normalMap = p->createTextureUnit ("normalMap"); - normalMap->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getTerrainNormalMap ()->getName()))); - normalMap->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - - Ogre::uint numLayersInThisPass = std::min(maxLayersInOnePass, terrain->getLayerCount()-layerOffset); - - // HACK: Terrain::getLayerBlendTextureIndex should be const, but it is not. - // Remove this once ogre got fixed. - Ogre::Terrain* nonconstTerrain = const_cast(terrain); - - // a blend map might be shared between two passes - // so we can't just use terrain->getBlendTextureCount() - Ogre::uint numBlendTextures=0; - std::vector blendTextures; - for (unsigned int layer=blendmapOffset; layergetBlendTextureName(nonconstTerrain->getLayerBlendTextureIndex( - static_cast(layerOffset+layer)).first); - if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) - { - blendTextures.push_back(blendTextureName); - ++numBlendTextures; - } - } - - p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numLayersInThisPass)))); - p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numBlendTextures)))); - - // blend maps - // the index of the first blend map used in this pass - int blendmapStart; - if (terrain->getLayerCount() == 1) // special case. if there's only one layer, we don't need blend maps at all - blendmapStart = 0; - else - blendmapStart = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+blendmapOffset)).first; - for (Ogre::uint i = 0; i < numBlendTextures; ++i) - { - sh::MaterialInstanceTextureUnit* blendTex = p->createTextureUnit ("blendMap" + Ogre::StringConverter::toString(i)); - blendTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getBlendTextureName(blendmapStart+i)))); - blendTex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - } - - // layer maps - for (Ogre::uint i = 0; i < numLayersInThisPass; ++i) - { - sh::MaterialInstanceTextureUnit* diffuseTex = p->createTextureUnit ("diffuseMap" + Ogre::StringConverter::toString(i)); - diffuseTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getLayerTextureName(layerOffset+i, 0)))); - - if (i+layerOffset > 0) - { - int blendTextureIndex = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+i)).first; - int blendTextureComponent = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+i)).second; - p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), - sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(blendTextureIndex-blendmapStart) + "." + getComponent(blendTextureComponent)))); - } - else - { - // just to make it shut up about blendmap_component_0 not existing in the first pass. - // it might be retrieved, but will never survive the preprocessing step. - p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), - sh::makeProperty (new sh::StringValue(""))); - } - } - - // shadow - for (Ogre::uint i = 0; i < 3; ++i) - { - sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); - shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); - } - - p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( - Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass + 2)))); - - // make sure the pass index is fed to the permutation handler, because blendmap components may be different - p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(pass))); - } - - return Ogre::MaterialManager::getSingleton().getByName(matName); - } - - void TerrainMaterial::Profile::setGlobalColourMapEnabled (bool enabled) - { - mGlobalColourMap = enabled; - mParent->_markChanged(); - } - - void TerrainMaterial::Profile::setGlobalColourMap (Ogre::Terrain* terrain, const std::string& name) - { - sh::Factory::getInstance ().setTextureAlias (terrain->getMaterialName () + "_colourMap", name); - } - - Ogre::MaterialPtr TerrainMaterial::Profile::generateForCompositeMap(const Ogre::Terrain* terrain) - { - throw std::runtime_error ("composite map not supported"); - } - - Ogre::uint8 TerrainMaterial::Profile::getMaxLayers(const Ogre::Terrain* terrain) const - { - return 255; - } - - int TerrainMaterial::Profile::getMaxLayersPerPass (const Ogre::Terrain* terrain) - { - // count the texture units free - Ogre::uint8 freeTextureUnits = 16; - // normalmap - --freeTextureUnits; - // colourmap - --freeTextureUnits; - // shadow - --freeTextureUnits; - --freeTextureUnits; - --freeTextureUnits; - - // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) - return static_cast(freeTextureUnits / (1.25f)); - } - - int TerrainMaterial::Profile::getRequiredPasses (const Ogre::Terrain* terrain) - { - int maxLayersPerPass = getMaxLayersPerPass(terrain); - assert(terrain->getLayerCount()); - assert(maxLayersPerPass); - return std::ceil(static_cast(terrain->getLayerCount()) / maxLayersPerPass); - } - - void TerrainMaterial::Profile::updateParams(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain) - { - } - - void TerrainMaterial::Profile::updateParamsForCompositeMap(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain) - { - } - - void TerrainMaterial::Profile::requestOptions(Ogre::Terrain* terrain) - { - terrain->_setMorphRequired(true); - terrain->_setNormalMapRequired(true); // global normal map - terrain->_setLightMapRequired(false); - terrain->_setCompositeMapRequired(false); - } - -} diff --git a/apps/openmw/mwrender/terrainmaterial.hpp b/apps/openmw/mwrender/terrainmaterial.hpp deleted file mode 100644 index c90499bae..000000000 --- a/apps/openmw/mwrender/terrainmaterial.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/* ------------------------------------------------------------------------------ -This source file is part of OGRE -(Object-oriented Graphics Rendering Engine) -For the latest info, see http://www.ogre3d.org/ - -Copyright (c) 2000-2011 Torus Knot Software Ltd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ------------------------------------------------------------------------------ -*/ - -#ifndef MWRENDER_TERRAINMATERIAL_H -#define MWRENDER_TERRAINMATERIAL_H - -#include "OgreTerrainPrerequisites.h" -#include "OgreTerrainMaterialGenerator.h" -#include "OgreGpuProgramParams.h" - -namespace sh -{ - class MaterialInstance; -} - -namespace MWRender -{ - - class TerrainMaterial : public Ogre::TerrainMaterialGenerator - { - public: - - class Profile : public Ogre::TerrainMaterialGenerator::Profile - { - public: - Profile(Ogre::TerrainMaterialGenerator* parent, const Ogre::String& name, const Ogre::String& desc); - virtual ~Profile(); - - virtual bool isVertexCompressionSupported() const { return false; } - - virtual Ogre::MaterialPtr generate(const Ogre::Terrain* terrain); - - virtual Ogre::MaterialPtr generateForCompositeMap(const Ogre::Terrain* terrain); - - virtual Ogre::uint8 getMaxLayers(const Ogre::Terrain* terrain) const; - - virtual void updateParams(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain); - - virtual void updateParamsForCompositeMap(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain); - - virtual void requestOptions(Ogre::Terrain* terrain); - - void setGlobalColourMapEnabled(bool enabled); - void setGlobalColourMap (Ogre::Terrain* terrain, const std::string& name); - virtual void setLightmapEnabled(bool) {} - - private: - sh::MaterialInstance* mMaterial; - - int getRequiredPasses (const Ogre::Terrain* terrain); - int getMaxLayersPerPass (const Ogre::Terrain* terrain); - - bool mGlobalColourMap; - - }; - - TerrainMaterial(); - }; - -} - - -#endif diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp new file mode 100644 index 000000000..318627fc7 --- /dev/null +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -0,0 +1,56 @@ +#include "terrainstorage.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" +#include "../mwworld/esmstore.hpp" + +namespace MWRender +{ + + Ogre::AxisAlignedBox TerrainStorage::getBounds() + { + int minX = 0, minY = 0, maxX = 0, maxY = 0; + + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + + MWWorld::Store::iterator it = esmStore.get().extBegin(); + for (; it != esmStore.get().extEnd(); ++it) + { + if (it->getGridX() < minX) + minX = it->getGridX(); + if (it->getGridX() > maxX) + maxX = it->getGridX(); + if (it->getGridY() < minY) + minY = it->getGridY(); + if (it->getGridY() > maxY) + maxY = it->getGridY(); + } + + // since grid coords are at cell origin, we need to add 1 cell + maxX += 1; + maxY += 1; + + return Ogre::AxisAlignedBox(minX, minY, 0, maxX, maxY, 0); + } + + ESM::Land* TerrainStorage::getLand(int cellX, int cellY) + { + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + ESM::Land* land = esmStore.get().search(cellX, cellY); + // Load the data we are definitely going to need + int mask = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + if (land && !land->isDataLoaded(mask)) + land->loadData(mask); + return land; + } + + const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + { + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + return esmStore.get().find(index, plugin); + } + +} diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp new file mode 100644 index 000000000..ebf5e26ab --- /dev/null +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -0,0 +1,22 @@ +#ifndef MWRENDER_TERRAINSTORAGE_H +#define MWRENDER_TERRAINSTORAGE_H + +#include + +namespace MWRender +{ + + class TerrainStorage : public Terrain::Storage + { + private: + virtual ESM::Land* getLand (int cellX, int cellY); + virtual const ESM::LandTexture* getLandTexture(int index, short plugin); + public: + virtual Ogre::AxisAlignedBox getBounds(); + ///< Get bounds in cell units + }; + +} + + +#endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 772eaf623..082551f37 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -202,7 +202,10 @@ Water::Water (Ogre::Camera *camera, RenderingManager* rend) : mWaterPlane = Plane(Vector3::UNIT_Z, 0); - MeshManager::getSingleton().createPlane("water", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, mWaterPlane, CELL_SIZE*5, CELL_SIZE * 5, 10, 10, true, 1, 3,3, Vector3::UNIT_Y); + int waterScale = 300; + + MeshManager::getSingleton().createPlane("water", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, mWaterPlane, + CELL_SIZE*5*waterScale, CELL_SIZE*5*waterScale, 10, 10, true, 1, 3*waterScale,3*waterScale, Vector3::UNIT_Y); mWater = mSceneMgr->createEntity("water"); mWater->setVisibilityFlags(RV_Water); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 5fa1400b1..03ddf2aa9 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -85,6 +85,7 @@ namespace MWWorld std::cout << "Unloading cell\n"; ListAndResetHandles functor; + /* (*iter)->forEach(functor); { // silence annoying g++ warning @@ -95,6 +96,7 @@ namespace MWWorld mPhysics->removeObject (node->getName()); } } + */ if ((*iter)->mCell->isExterior()) { @@ -148,7 +150,7 @@ namespace MWWorld // ... then references. This is important for adjustPosition to work correctly. /// \todo rescale depending on the state of a new GMST - insertCell (*cell, true); + //insertCell (*cell, true); mRendering.cellAdded (cell); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index bd234c0b6..ced4b5faa 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1,5 +1,7 @@ #include "worldimp.hpp" +#include + #include #include @@ -833,7 +835,7 @@ namespace MWWorld bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || mWorldScene->isCellActive(*currCell); - if (*currCell != newCell) + if (false ) //*currCell != newCell) { removeContainerScripts(ptr); @@ -1024,7 +1026,7 @@ namespace MWWorld return; } - float terrainHeight = mRendering->getTerrainHeightAt(pos); + float terrainHeight = -std::numeric_limits().max();// mRendering->getTerrainHeightAt(pos); if (pos.z < terrainHeight) pos.z = terrainHeight; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 529891b4c..baf905aa7 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -65,6 +65,10 @@ add_component_dir (interpreter add_component_dir (translation translation ) + +add_component_dir (terrain + quadtreenode chunk terrain storage material + ) find_package(Qt4 COMPONENTS QtCore QtGui) diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index c1cce5e7e..9c1fd1f5c 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -70,7 +70,7 @@ struct Land }; #pragma pack(pop) - typedef uint8_t VNML[LAND_NUM_VERTS * 3]; + typedef signed char VNML[LAND_NUM_VERTS * 3]; struct LandData { diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp new file mode 100644 index 000000000..c9a364dc4 --- /dev/null +++ b/components/terrain/chunk.cpp @@ -0,0 +1,171 @@ +#include "chunk.hpp" + +#include +#include +#include +#include +#include + +#include "quadtreenode.hpp" +#include "terrain.hpp" +#include "storage.hpp" + +namespace Terrain +{ + + Chunk::Chunk(QuadTreeNode* node, short lodLevel) + : mNode(node) + , mVertexLod(lodLevel) + , mAdditionalLod(0) + { + mVertexData = OGRE_NEW Ogre::VertexData; + mVertexData->vertexStart = 0; + + // Set the total number of vertices + size_t numVertsOneSide = mNode->getSize() * (ESM::Land::LAND_SIZE-1); + numVertsOneSide /= std::pow(2, lodLevel); + numVertsOneSide += 1; + assert((int)numVertsOneSide == ESM::Land::LAND_SIZE); + mVertexData->vertexCount = numVertsOneSide * numVertsOneSide; + + // Set up the vertex declaration, which specifies the info for each vertex (normals, colors, UVs, etc) + Ogre::VertexDeclaration* vertexDecl = mVertexData->vertexDeclaration; + + Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr(); + size_t nextBuffer = 0; + + // Positions + vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT3, Ogre::VES_POSITION); + mVertexBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3), + mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); + // Normals + vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT3, Ogre::VES_NORMAL); + mNormalBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3), + mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); + + // UV texture coordinates + vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT2, + Ogre::VES_TEXTURE_COORDINATES, 0); + Ogre::HardwareVertexBufferSharedPtr uvBuf = mNode->getTerrain()->getVertexBuffer(numVertsOneSide); + + // Colours + vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_COLOUR, Ogre::VES_DIFFUSE); + mColourBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR), + mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); + + + mNode->getTerrain()->getStorage()->fillVertexBuffers(lodLevel, mNode->getSize(), mNode->getCenter(), + mVertexBuffer, mNormalBuffer, mColourBuffer); + + mVertexData->vertexBufferBinding->setBinding(0, mVertexBuffer); + mVertexData->vertexBufferBinding->setBinding(1, mNormalBuffer); + mVertexData->vertexBufferBinding->setBinding(2, uvBuf); + mVertexData->vertexBufferBinding->setBinding(3, mColourBuffer); + + mIndexData = OGRE_NEW Ogre::IndexData(); + mIndexData->indexStart = 0; + } + + void Chunk::updateIndexBuffer() + { + // Fetch a suitable index buffer (which may be shared) + size_t ourLod = mVertexLod + mAdditionalLod; + + int flags = 0; + + for (int i=0; i<4; ++i) + { + QuadTreeNode* neighbour = mNode->searchNeighbour((Direction)i); + + // If the neighbour isn't currently rendering itself, + // go up until we find one. NOTE: We don't need to go down, + // because in that case neighbour's detail would be higher than + // our detail and the neighbour would handle stitching by itself. + while (neighbour && !neighbour->hasChunk()) + neighbour = neighbour->getParent(); + + size_t lod = 0; + if (neighbour) + lod = neighbour->getActualLodLevel(); + + if (lod <= ourLod) // We only need to worry about neighbours less detailed than we are - + lod = 0; // neighbours with more detail will do the stitching themselves + + // Use 4 bits for each LOD delta + if (lod > 0) + { + assert (lod - ourLod < std::pow(2,4)); + flags |= int(lod - ourLod) << (4*i); + } + } + + flags |= ((int)mAdditionalLod) << (4*4); + + size_t numIndices; + mIndexBuffer = mNode->getTerrain()->getIndexBuffer(flags, numIndices); + mIndexData->indexCount = numIndices; + mIndexData->indexBuffer = mIndexBuffer; + } + + Chunk::~Chunk() + { + OGRE_DELETE mVertexData; + OGRE_DELETE mIndexData; + } + + void Chunk::setMaterial(const Ogre::MaterialPtr &material) + { + mMaterial = material; + } + + const Ogre::AxisAlignedBox& Chunk::getBoundingBox(void) const + { + return mNode->getBoundingBox(); + } + + Ogre::Real Chunk::getBoundingRadius(void) const + { + return mNode->getBoundingBox().getHalfSize().length(); + } + + void Chunk::_updateRenderQueue(Ogre::RenderQueue* queue) + { + queue->addRenderable(this, mRenderQueueID); + } + + void Chunk::visitRenderables(Ogre::Renderable::Visitor* visitor, + bool debugRenderables) + { + visitor->visit(this, 0, false); + } + + const Ogre::MaterialPtr& Chunk::getMaterial(void) const + { + return mMaterial; + } + + void Chunk::getRenderOperation(Ogre::RenderOperation& op) + { + assert (!mIndexBuffer.isNull() && "Trying to render, but no index buffer set!"); + op.useIndexes = true; + op.operationType = Ogre::RenderOperation::OT_TRIANGLE_LIST; + op.vertexData = mVertexData; + op.indexData = mIndexData; + } + + void Chunk::getWorldTransforms(Ogre::Matrix4* xform) const + { + *xform = getParentSceneNode()->_getFullTransform(); + } + + Ogre::Real Chunk::getSquaredViewDepth(const Ogre::Camera* cam) const + { + return getParentSceneNode()->getSquaredViewDepth(cam); + } + + const Ogre::LightList& Chunk::getLights(void) const + { + return queryLights(); + } + +} diff --git a/components/terrain/chunk.hpp b/components/terrain/chunk.hpp new file mode 100644 index 000000000..d74c65ba6 --- /dev/null +++ b/components/terrain/chunk.hpp @@ -0,0 +1,63 @@ +#ifndef COMPONENTS_TERRAIN_TERRAINBATCH_H +#define COMPONENTS_TERRAIN_TERRAINBATCH_H + +#include +#include + +namespace Terrain +{ + + class QuadTreeNode; + + /** + * @brief Renders a chunk of terrain, either using alpha splatting or a composite map. + */ + class Chunk : public Ogre::Renderable, public Ogre::MovableObject + { + public: + /// @param lodLevel LOD level for the vertex buffer. + Chunk (QuadTreeNode* node, short lodLevel); + virtual ~Chunk(); + + void setMaterial (const Ogre::MaterialPtr& material); + + /// Set additional LOD applied on top of vertex LOD. \n + /// This is achieved by changing the index buffer to omit vertices. + void setAdditionalLod (size_t lod) { mAdditionalLod = lod; } + size_t getAdditionalLod() { return mAdditionalLod; } + + void updateIndexBuffer(); + + // Inherited from MovableObject + virtual const Ogre::String& getMovableType(void) const { static Ogre::String t = "MW_TERRAIN"; return t; } + virtual const Ogre::AxisAlignedBox& getBoundingBox(void) const; + virtual Ogre::Real getBoundingRadius(void) const; + virtual void _updateRenderQueue(Ogre::RenderQueue* queue); + virtual void visitRenderables(Renderable::Visitor* visitor, + bool debugRenderables = false); + + // Inherited from Renderable + virtual const Ogre::MaterialPtr& getMaterial(void) const; + virtual void getRenderOperation(Ogre::RenderOperation& op); + virtual void getWorldTransforms(Ogre::Matrix4* xform) const; + virtual Ogre::Real getSquaredViewDepth(const Ogre::Camera* cam) const; + virtual const Ogre::LightList& getLights(void) const; + + private: + QuadTreeNode* mNode; + Ogre::MaterialPtr mMaterial; + + size_t mVertexLod; + size_t mAdditionalLod; + + Ogre::VertexData* mVertexData; + Ogre::IndexData* mIndexData; + Ogre::HardwareVertexBufferSharedPtr mVertexBuffer; + Ogre::HardwareVertexBufferSharedPtr mNormalBuffer; + Ogre::HardwareVertexBufferSharedPtr mColourBuffer; + Ogre::HardwareIndexBufferSharedPtr mIndexBuffer; + }; + +} + +#endif diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp new file mode 100644 index 000000000..bd7501576 --- /dev/null +++ b/components/terrain/material.cpp @@ -0,0 +1,291 @@ +#include "material.hpp" + +#include +#include +#include + +#include + +namespace +{ + +int getBlendmapIndexForLayer (int layerIndex) +{ + return std::floor((layerIndex-1)/4.f); +} + +std::string getBlendmapComponentForLayer (int layerIndex) +{ + int n = (layerIndex-1)%4; + if (n == 0) + return "x"; + if (n == 1) + return "y"; + if (n == 2) + return "z"; + else + return "w"; +} + +} + +namespace Terrain +{ + + MaterialGenerator::MaterialGenerator(bool shaders) + : mShaders(shaders) + { + + } + + int MaterialGenerator::getMaxLayersPerPass () + { + // count the texture units free + Ogre::uint8 freeTextureUnits = 16; + + // first layer doesn't need blendmap + --freeTextureUnits; + + // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) + return static_cast(freeTextureUnits / (1.25f)) + 1; + } + + int MaterialGenerator::getRequiredPasses () + { + int maxLayersPerPass = getMaxLayersPerPass(); + return std::max(1.f, std::ceil(static_cast(mLayerList.size()) / maxLayersPerPass)); + } + + Ogre::MaterialPtr MaterialGenerator::generate(Ogre::MaterialPtr mat) + { + return create(mat, false, false); + } + + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMapRTT(Ogre::MaterialPtr mat) + { + return create(mat, true, false); + } + + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMap(Ogre::MaterialPtr mat) + { + return create(mat, false, true); + } + + Ogre::MaterialPtr MaterialGenerator::create(Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap) + { + assert(!renderCompositeMap || !displayCompositeMap); + if (!mat.isNull()) + { + sh::Factory::getInstance().destroyMaterialInstance(mat->getName()); + Ogre::MaterialManager::getSingleton().remove(mat->getName()); + } + + static int count = 0; + std::stringstream name; + name << "terrain/mat" << count++; + + if (!mShaders) + { + mat = Ogre::MaterialManager::getSingleton().create(name.str(), + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + Ogre::Technique* technique = mat->getTechnique(0); + technique->removeAllPasses(); + + if (displayCompositeMap) + { + Ogre::Pass* pass = technique->createPass(); + pass->setVertexColourTracking(Ogre::TVC_AMBIENT|Ogre::TVC_DIFFUSE); + pass->createTextureUnitState(mCompositeMap)->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); + } + else + { + assert(mLayerList.size() == mBlendmapList.size()+1); + std::vector::iterator blend = mBlendmapList.begin(); + for (std::vector::iterator layer = mLayerList.begin(); layer != mLayerList.end(); ++layer) + { + Ogre::Pass* pass = technique->createPass(); + pass->setLightingEnabled(false); + pass->setVertexColourTracking(Ogre::TVC_NONE); + + bool first = (layer == mLayerList.begin()); + + Ogre::TextureUnitState* tus; + + if (!first) + { + pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA); + pass->setDepthFunction(Ogre::CMPF_EQUAL); + + tus = pass->createTextureUnitState((*blend)->getName()); + tus->setAlphaOperation(Ogre::LBX_BLEND_TEXTURE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_TEXTURE); + tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_TEXTURE); + tus->setIsAlpha(true); + tus->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); + + float scale = (16/(16.f+1.f)); + float scroll = 1/16.f*0.5; + tus->setTextureScale(scale,scale); + tus->setTextureScroll(-scroll,-scroll); + } + + // Add the actual layer texture on top of the alpha map. + tus = pass->createTextureUnitState("textures\\" + *layer); + if (!first) + tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_CURRENT); + + tus->setTextureScale(1/16.f,1/16.f); + + if (!first) + ++blend; + } + + if (!renderCompositeMap) + { + Ogre::Pass* lightingPass = technique->createPass(); + lightingPass->setSceneBlending(Ogre::SBT_MODULATE); + lightingPass->setVertexColourTracking(Ogre::TVC_AMBIENT|Ogre::TVC_DIFFUSE); + } + } + + return mat; + } + else + { + + sh::MaterialInstance* material = sh::Factory::getInstance().createMaterialInstance (name.str()); + material->setProperty ("allow_fixed_function", sh::makeProperty(new sh::BooleanValue(false))); + + if (displayCompositeMap) + { + sh::MaterialInstancePass* p = material->createPass (); + + p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); + p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); + p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(true))); + p->mShaderProperties.setProperty ("render_composite_map", sh::makeProperty(new sh::BooleanValue(false))); + p->mShaderProperties.setProperty ("display_composite_map", sh::makeProperty(new sh::BooleanValue(true))); + p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue("0"))); + p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue("0"))); + + sh::MaterialInstanceTextureUnit* tex = p->createTextureUnit ("compositeMap"); + tex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(mCompositeMap))); + tex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); + + // shadow. TODO: repeated, put in function + for (Ogre::uint i = 0; i < 3; ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } + + p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( + Ogre::StringConverter::toString(1)))); + + p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(0))); + } + else + { + + int numPasses = getRequiredPasses(); + assert(numPasses); + int maxLayersInOnePass = getMaxLayersPerPass(); + + for (int pass=0; passcreatePass (); + + p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); + p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); + if (pass != 0) + { + p->setProperty ("scene_blend", sh::makeProperty(new sh::StringValue("alpha_blend"))); + // Only write if depth is equal to the depth value written by the previous pass. + p->setProperty ("depth_func", sh::makeProperty(new sh::StringValue("equal"))); + } + + p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(pass == 0))); + p->mShaderProperties.setProperty ("render_composite_map", sh::makeProperty(new sh::BooleanValue(renderCompositeMap))); + p->mShaderProperties.setProperty ("display_composite_map", sh::makeProperty(new sh::BooleanValue(displayCompositeMap))); + + Ogre::uint numLayersInThisPass = std::min(maxLayersInOnePass, (int)mLayerList.size()-layerOffset); + + // a blend map might be shared between two passes + Ogre::uint numBlendTextures=0; + std::vector blendTextures; + for (unsigned int layer=blendmapOffset; layergetName(); + if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) + { + blendTextures.push_back(blendTextureName); + ++numBlendTextures; + } + } + + p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numLayersInThisPass)))); + p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numBlendTextures)))); + + // blend maps + // the index of the first blend map used in this pass + int blendmapStart; + if (mLayerList.size() == 1) // special case. if there's only one layer, we don't need blend maps at all + blendmapStart = 0; + else + blendmapStart = getBlendmapIndexForLayer(layerOffset+blendmapOffset); + for (Ogre::uint i = 0; i < numBlendTextures; ++i) + { + sh::MaterialInstanceTextureUnit* blendTex = p->createTextureUnit ("blendMap" + Ogre::StringConverter::toString(i)); + blendTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(mBlendmapList[blendmapStart+i]->getName()))); + blendTex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); + } + + // layer maps + for (Ogre::uint i = 0; i < numLayersInThisPass; ++i) + { + sh::MaterialInstanceTextureUnit* diffuseTex = p->createTextureUnit ("diffuseMap" + Ogre::StringConverter::toString(i)); + diffuseTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue("textures\\"+mLayerList[layerOffset+i]))); + + if (i+layerOffset > 0) + { + int blendTextureIndex = getBlendmapIndexForLayer(layerOffset+i); + std::string blendTextureComponent = getBlendmapComponentForLayer(layerOffset+i); + p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), + sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(blendTextureIndex-blendmapStart) + "." + blendTextureComponent))); + } + else + { + // just to make it shut up about blendmap_component_0 not existing in the first pass. + // it might be retrieved, but will never survive the preprocessing step. + p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), + sh::makeProperty (new sh::StringValue(""))); + } + } + + // shadow + for (Ogre::uint i = 0; i < 3; ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } + + p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( + Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass)))); + + // Make sure the pass index is fed to the permutation handler, because blendmap components may be different + p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(pass))); + } + } + } + return Ogre::MaterialManager::getSingleton().getByName(name.str()); + } + +} diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp new file mode 100644 index 000000000..749e806ad --- /dev/null +++ b/components/terrain/material.hpp @@ -0,0 +1,54 @@ +#ifndef COMPONENTS_TERRAIN_MATERIAL_H +#define COMPONENTS_TERRAIN_MATERIAL_H + +#include + +namespace Terrain +{ + + class MaterialGenerator + { + public: + /// @param layerList layer textures + /// @param blendmapList blend textures + /// @param shaders Whether to use shaders. With a shader, blendmap packing can be used (4 channels instead of one), + /// so if this parameter is true, then the supplied blend maps are expected to be packed. + MaterialGenerator (bool shaders); + + void setLayerList (const std::vector& layerList) { mLayerList = layerList; } + bool hasLayers() { return mLayerList.size(); } + void setBlendmapList (const std::vector& blendmapList) { mBlendmapList = blendmapList; } + void setCompositeMap (const std::string& name) { mCompositeMap = name; } + + /// Creates a material suitable for displaying a chunk of terrain using alpha-blending. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generate (Ogre::MaterialPtr mat); + + /// Creates a material suitable for displaying a chunk of terrain using a ready-made composite map. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generateForCompositeMap (Ogre::MaterialPtr mat); + + /// Creates a material suitable for rendering composite maps, i.e. for "baking" several layer textures + /// into one. The main difference compared to a normal material is that no shading is applied at this point. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generateForCompositeMapRTT (Ogre::MaterialPtr mat); + + private: + Ogre::MaterialPtr create (Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap); + + int getRequiredPasses (); + int getMaxLayersPerPass (); + + int mNumLayers; + std::vector mLayerList; + std::vector mBlendmapList; + std::string mCompositeMap; + bool mShaders; + }; + +} + +#endif diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp new file mode 100644 index 000000000..8ebfafeb4 --- /dev/null +++ b/components/terrain/quadtreenode.cpp @@ -0,0 +1,387 @@ +#include "quadtreenode.hpp" + +#include +#include + +#include "terrain.hpp" +#include "chunk.hpp" +#include "storage.hpp" + +#include "material.hpp" + +using namespace Terrain; + +namespace +{ + + // Utility functions for neighbour finding algorithm + ChildDirection reflect(ChildDirection dir, Direction dir2) + { + assert(dir != Root); + + const int lookupTable[4][4] = + { + // NW NE SW SE + { SW, SE, NW, NE }, // N + { NE, NW, SE, SW }, // E + { SW, SE, NW, NE }, // S + { NE, NW, SE, SW } // W + }; + return (ChildDirection)lookupTable[dir2][dir]; + } + + bool adjacent(ChildDirection dir, Direction dir2) + { + assert(dir != Root); + const bool lookupTable[4][4] = + { + // NW NE SW SE + { true, true, false, false }, // N + { false, true, false, true }, // E + { false, false, true, true }, // S + { true, false, true, false } // W + }; + return lookupTable[dir2][dir]; + } + + // Algorithm described by Hanan Samet - 'Neighbour Finding in Quadtrees' + // http://www.cs.umd.edu/~hjs/pubs/SametPRIP81.pdf + Terrain::QuadTreeNode* searchNeighbourRecursive (Terrain::QuadTreeNode* currentNode, Terrain::Direction dir) + { + if (!currentNode->getParent()) + return NULL; // Arrived at root node, the root node does not have neighbours + + Terrain::QuadTreeNode* nextNode; + if (adjacent(currentNode->getDirection(), dir)) + nextNode = searchNeighbourRecursive(currentNode->getParent(), dir); + else + nextNode = currentNode->getParent(); + + if (nextNode && nextNode->hasChildren()) + return nextNode->getChild(reflect(currentNode->getDirection(), dir)); + else + return NULL; + } + + + // Ogre::AxisAlignedBox::distance is broken in 1.8. + Ogre::Real distance(const Ogre::AxisAlignedBox& box, const Ogre::Vector3& v) + { + + if (box.contains(v)) + return 0; + else + { + Ogre::Vector3 maxDist(0,0,0); + const Ogre::Vector3& minimum = box.getMinimum(); + const Ogre::Vector3& maximum = box.getMaximum(); + + if (v.x < minimum.x) + maxDist.x = minimum.x - v.x; + else if (v.x > maximum.x) + maxDist.x = v.x - maximum.x; + + if (v.y < minimum.y) + maxDist.y = minimum.y - v.y; + else if (v.y > maximum.y) + maxDist.y = v.y - maximum.y; + + if (v.z < minimum.z) + maxDist.z = minimum.z - v.z; + else if (v.z > maximum.z) + maxDist.z = v.z - maximum.z; + + return maxDist.length(); + } + } + + // Create a 2D quad + void makeQuad(Ogre::SceneManager* sceneMgr, float left, float top, float right, float bottom, Ogre::MaterialPtr material) + { + Ogre::ManualObject* manual = sceneMgr->createManualObject(); + + // Use identity view/projection matrices to get a 2d quad + manual->setUseIdentityProjection(true); + manual->setUseIdentityView(true); + + manual->begin(material->getName()); + + float normLeft = left*2-1; + float normTop = top*2-1; + float normRight = right*2-1; + float normBottom = bottom*2-1; + + manual->position(normLeft, normTop, 0.0); + manual->textureCoord(0, 1); + manual->position(normRight, normTop, 0.0); + manual->textureCoord(1, 1); + manual->position(normRight, normBottom, 0.0); + manual->textureCoord(1, 0); + manual->position(normLeft, normBottom, 0.0); + manual->textureCoord(0, 0); + + manual->quad(0,1,2,3); + + manual->end(); + + Ogre::AxisAlignedBox aabInf; + aabInf.setInfinite(); + manual->setBoundingBox(aabInf); + + sceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(manual); + } +} + +QuadTreeNode::QuadTreeNode(Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) + : mSize(size) + , mCenter(center) + , mParent(parent) + , mDirection(dir) + , mIsDummy(false) + , mSceneNode(NULL) + , mTerrain(terrain) + , mChunk(NULL) + , mMaterialGenerator(NULL) +{ + mBounds.setNull(); + for (int i=0; i<4; ++i) + mChildren[i] = NULL; + + mSceneNode = mTerrain->getSceneManager()->getRootSceneNode()->createChildSceneNode( + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + + mLodLevel = log2(mSize); + + mMaterialGenerator = new MaterialGenerator(true); +} + +void QuadTreeNode::createChild(ChildDirection id, float size, const Ogre::Vector2 ¢er) +{ + mChildren[id] = new QuadTreeNode(mTerrain, id, size, center, this); +} + +QuadTreeNode::~QuadTreeNode() +{ + for (int i=0; i<4; ++i) + delete mChildren[i]; + delete mChunk; + delete mMaterialGenerator; +} + +QuadTreeNode* QuadTreeNode::searchNeighbour(Direction dir) +{ + return searchNeighbourRecursive(this, dir); +} + +const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox() +{ + if (mIsDummy) + return Ogre::AxisAlignedBox::BOX_NULL; + if (mBounds.isNull()) + { + if (hasChildren()) + { + // X and Y are obvious, just need Z + float min = std::numeric_limits().max(); + float max = -std::numeric_limits().max(); + for (int i=0; i<4; ++i) + { + QuadTreeNode* child = getChild((ChildDirection)i); + float v = child->getBoundingBox().getMaximum().z; + if (v > max) + max = v; + v = child->getBoundingBox().getMinimum().z; + if (v < min) + min = v; + } + mBounds = Ogre::AxisAlignedBox (Ogre::Vector3(-mSize/2*8192, -mSize/2*8192, min), + Ogre::Vector3(mSize/2*8192, mSize/2*8192, max)); + } + else + throw std::runtime_error("Leaf node should have bounds set!"); + } + return mBounds; +} + +void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) +{ + const Ogre::AxisAlignedBox& bounds = getBoundingBox(); + if (bounds.isNull()) + return; + + Ogre::AxisAlignedBox worldBounds (bounds.getMinimum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0), + bounds.getMaximum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + + float dist = distance(worldBounds, cameraPos); + /// \todo implement error metrics or some other means of not using arbitrary values + size_t wantedLod = 0; + if (dist > 8192*1) + wantedLod = 1; + if (dist > 8192*2) + wantedLod = 2; + if (dist > 8192*5) + wantedLod = 3; + if (dist > 8192*12) + wantedLod = 4; + if (dist > 8192*32) + wantedLod = 5; + if (dist > 8192*64) + wantedLod = 6; + + if (mSize <= mTerrain->getMaxBatchSize() && mLodLevel <= wantedLod) + { + // Wanted LOD is small enough to render this node in one chunk + if (!mChunk) + { + mChunk = new Chunk(this, mLodLevel); + mChunk->setVisibilityFlags(mTerrain->getVisiblityFlags()); + mChunk->setCastShadows(true); + mSceneNode->attachObject(mChunk); + if (mSize == 1) + { + ensureLayerInfo(); + mChunk->setMaterial(mMaterialGenerator->generate(mChunk->getMaterial())); + } + else + { + ensureCompositeMap(); + mMaterialGenerator->setCompositeMap(mCompositeMap->getName()); + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(mChunk->getMaterial())); + } + } + + + mChunk->setAdditionalLod(wantedLod - mLodLevel); + mChunk->setVisible(true); + + if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->removeChunks(); + } + } + else + { + // Wanted LOD is too detailed to be rendered in one chunk, + // so split it up by delegating to child nodes + if (mChunk) + mChunk->setVisible(false); + assert(hasChildren() && "Leaf node's LOD needs to be 0"); + for (int i=0; i<4; ++i) + mChildren[i]->update(cameraPos); + } +} + +void QuadTreeNode::removeChunks() +{ + if (mChunk) + mChunk->setVisible(false); + if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->removeChunks(); + } +} + +void QuadTreeNode::updateIndexBuffers() +{ + if (hasChunk()) + mChunk->updateIndexBuffer(); + else if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->updateIndexBuffers(); + } +} + +bool QuadTreeNode::hasChunk() +{ + return mChunk && mChunk->getVisible(); +} + +size_t QuadTreeNode::getActualLodLevel() +{ + assert(hasChunk() && "Can't get actual LOD level if this node has no render chunk"); + return mLodLevel + mChunk->getAdditionalLod(); +} + +void QuadTreeNode::ensureLayerInfo() +{ + if (mMaterialGenerator->hasLayers()) + return; + + std::vector blendmaps; + std::vector layerList; + mTerrain->getStorage()->getBlendmaps(mSize, mCenter, true, blendmaps, layerList); + + mMaterialGenerator->setLayerList(layerList); + mMaterialGenerator->setBlendmapList(blendmaps); +} + +void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) +{ + Ogre::SceneManager* sceneMgr = mTerrain->getCompositeMapSceneManager(); + + if (mIsDummy) + { + MaterialGenerator matGen(true); + std::vector layer; + layer.push_back("_land_default.dds"); + matGen.setLayerList(layer); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generate(Ogre::MaterialPtr())); + return; + } + if (mSize > 1) + { + assert(hasChildren()); + + // 0,0 -------- 1,0 + // | | | + // |-----|------| + // | | | + // 0,1 -------- 1,1 + + float halfW = area.width()/2.f; + float halfH = area.height()/2.f; + mChildren[NW]->prepareForCompositeMap(Ogre::TRect(area.left, area.top, area.right-halfW, area.bottom-halfH)); + mChildren[NE]->prepareForCompositeMap(Ogre::TRect(area.left+halfW, area.top, area.right, area.bottom-halfH)); + mChildren[SW]->prepareForCompositeMap(Ogre::TRect(area.left, area.top+halfH, area.right-halfW, area.bottom)); + mChildren[SE]->prepareForCompositeMap(Ogre::TRect(area.left+halfW, area.top+halfH, area.right, area.bottom)); + } + else + { + ensureLayerInfo(); + + Ogre::MaterialPtr material = mMaterialGenerator->generateForCompositeMapRTT(Ogre::MaterialPtr()); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, material); + } +} + + +bool QuadTreeNode::hasCompositeMap() +{ + return !mCompositeMap.isNull(); +} + +void QuadTreeNode::ensureCompositeMap() +{ + if (!mCompositeMap.isNull()) + return; + + static int i=0; + std::stringstream name; + name << "terrain/comp" << i++; + + const int size = 128; + mCompositeMap = Ogre::TextureManager::getSingleton().createManual( + name.str(), Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, size, size, Ogre::MIP_DEFAULT, Ogre::PF_A8B8G8R8); + + // Create quads for each cell + prepareForCompositeMap(Ogre::TRect(0,0,1,1)); + + mTerrain->renderCompositeMap(mCompositeMap); + + mTerrain->clearCompositeMapSceneManager(); + +} diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp new file mode 100644 index 000000000..b68d6ab82 --- /dev/null +++ b/components/terrain/quadtreenode.hpp @@ -0,0 +1,141 @@ +#ifndef COMPONENTS_TERRAIN_QUADTREENODE_H +#define COMPONENTS_TERRAIN_QUADTREENODE_H + +#include +#include +#include + +namespace Ogre +{ + class Rectangle2D; +} + +namespace Terrain +{ + class Terrain; + class Chunk; + class MaterialGenerator; + + enum Direction + { + North = 0, + East = 1, + South = 2, + West = 3 + }; + + enum ChildDirection + { + NW = 0, + NE = 1, + SW = 2, + SE = 3, + Root + }; + + /** + * @brief A node in the quad tree for our terrain. Depending on LOD, + * a node can either choose to render itself in one batch (merging its children), + * or delegate the render process to its children, rendering each child in at least one batch. + */ + class QuadTreeNode + { + public: + /// @param terrain + /// @param dir relative to parent, or Root if we are the root node + /// @param size size (in *cell* units!) + /// @param center center (in *cell* units!) + /// @param parent parent node + QuadTreeNode (Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); + ~QuadTreeNode(); + + /// @note takes ownership of \a child + void createChild (ChildDirection id, float size, const Ogre::Vector2& center); + + /// Mark this node as a dummy node. This can happen if the terrain size isn't a power of two. + /// For the QuadTree to work, we need to round the size up to a power of two, which means we'll + /// end up with empty nodes that don't actually render anything. + void markAsDummy() { mIsDummy = true; } + bool isDummy() { return mIsDummy; } + + QuadTreeNode* getParent() { return mParent; } + + int getSize() { return mSize; } + Ogre::Vector2 getCenter() { return mCenter; } + + bool hasChildren() { return mChildren[0] != 0; } + QuadTreeNode* getChild(ChildDirection dir) { return mChildren[dir]; } + + /// Search for a neighbour node in this direction + QuadTreeNode* searchNeighbour (Direction dir); + + /// Returns our direction relative to the parent node, or Root if we are the root node. + ChildDirection getDirection() { return mDirection; } + + /// Set bounding box in local coordinates. Should be done at load time for leaf nodes. + /// Other nodes can merge AABB of child nodes. + void setBoundingBox (const Ogre::AxisAlignedBox& box) { mBounds = box; } + + /// Get bounding box in local coordinates + const Ogre::AxisAlignedBox& getBoundingBox(); + + Terrain* getTerrain() { return mTerrain; } + + /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. + void update (const Ogre::Vector3& cameraPos); + + /// Adjust index buffers of chunks to stitch together chunks of different LOD, so that cracks are avoided. + /// Call after QuadTreeNode::update! + void updateIndexBuffers(); + + /// Remove chunks rendered by this node and all its children + void removeChunks(); + + /// Get the effective LOD level if this node was rendered in one chunk + /// with ESM::Land::LAND_SIZE^2 vertices + size_t getNativeLodLevel() { return mLodLevel; } + + /// Get the effective current LOD level used by the chunk rendering this node + size_t getActualLodLevel(); + + /// Is this node currently configured to render itself? + bool hasChunk(); + + bool hasCompositeMap(); + + /// Add a textured quad to a specific 2d area in the composite map scenemanager. + /// Only nodes with size <= 1 can be rendered with alpha blending, so larger nodes will simply + /// call this method on their children. + /// @param area area in image space to put the quad + /// @param quads collect quads here so they can be deleted later + void prepareForCompositeMap(Ogre::TRect area); + + private: + // Stored here for convenience in case we need layer list again + MaterialGenerator* mMaterialGenerator; + + bool mIsDummy; + float mSize; + size_t mLodLevel; // LOD if we were to render this node in one chunk + Ogre::AxisAlignedBox mBounds; + ChildDirection mDirection; + Ogre::Vector2 mCenter; + + Ogre::SceneNode* mSceneNode; + + QuadTreeNode* mParent; + QuadTreeNode* mChildren[4]; + + Chunk* mChunk; + + Terrain* mTerrain; + + Ogre::TexturePtr mCompositeMap; + + void ensureLayerInfo(); + void ensureCompositeMap(); + }; + +} + +#endif diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp new file mode 100644 index 000000000..eb2763d94 --- /dev/null +++ b/components/terrain/storage.cpp @@ -0,0 +1,318 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Terrain +{ + + struct VertexElement + { + Ogre::Vector3 pos; + Ogre::Vector3 normal; + Ogre::ColourValue colour; + }; + + bool Storage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) + { + assert (size <= 1 && "Storage::getMinMaxHeights, chunk size should be <= 1 cell"); + + /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int cellX = origin.x; + int cellY = origin.y; + + const ESM::Land* land = getLand(cellX, cellY); + if (!land) + return false; + + min = std::numeric_limits().max(); + max = -std::numeric_limits().max(); + for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + if (h > max) + max = h; + if (h < min) + min = h; + } + } + return true; + } + + void Storage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) + { + if (col == ESM::Land::LAND_SIZE-1) + { + ++cellY; + col = 0; + } + if (row == ESM::Land::LAND_SIZE-1) + { + ++cellX; + row = 0; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mHasData) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + } + } + + void Storage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, + Ogre::HardwareVertexBufferSharedPtr vertexBuffer, + Ogre::HardwareVertexBufferSharedPtr normalBuffer, + Ogre::HardwareVertexBufferSharedPtr colourBuffer) + { + // LOD level n means every 2^n-th vertex is kept + size_t increment = std::pow(2, lodLevel); + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int startX = origin.x; + int startY = origin.y; + + size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; + + std::vector colors; + colors.resize(numVerts*numVerts*4); + std::vector positions; + positions.resize(numVerts*numVerts*3); + std::vector normals; + normals.resize(numVerts*numVerts*3); + + Ogre::Vector3 normal; + Ogre::ColourValue color; + + float vertY; + float vertX; + + float vertY_ = 0; // of current cell corner + for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) + { + float vertX_ = 0; // of current cell corner + for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) + { + ESM::Land* land = getLand(cellX, cellY); + if (land && !land->mHasData) + land = NULL; + bool hasColors = land && land->mLandData->mUsingColours; + + int rowStart = 0; + int colStart = 0; + // Skip the first row / column unless we're at a chunk edge, + // since this row / column is already contained in a previous cell + if (colStart == 0 && vertY_ != 0) + colStart += increment; + if (rowStart == 0 && vertX_ != 0) + rowStart += increment; + + vertY = vertY_; + for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + else + positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; + + if (land) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + // Normals don't connect seamlessly between cells - wtf? + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixNormal(normal, cellX, cellY, col, row); + // z < 0 should never happen, but it does - I hate this data set... + if (normal.z < 0) + normal *= -1; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + + normals[vertX*numVerts*3 + vertY*3] = normal.x; + normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; + normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; + + if (hasColors) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + color.a = 1; + Ogre::uint32 rsColor; + Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); + memcpy(&colors[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); + + ++vertX; + } + ++vertY; + } + vertX_ = vertX; + } + vertY_ = vertY; + + assert(vertX_ == numVerts); // Ensure we covered whole area + } + assert(vertY_ == numVerts); // Ensure we covered whole area + + vertexBuffer->writeData(0, vertexBuffer->getSizeInBytes(), &positions[0], true); + normalBuffer->writeData(0, normalBuffer->getSizeInBytes(), &normals[0], true); + colourBuffer->writeData(0, colourBuffer->getSizeInBytes(), &colors[0], true); + } + + Storage::UniqueTextureId Storage::getVtexIndexAt(int cellX, int cellY, + int x, int y) + { + // If we're at the last row (or last column), we need to get the texture from the neighbour cell + // to get consistent blending at the border + if (x >= ESM::Land::LAND_TEXTURE_SIZE) + { + cellX++; + x -= ESM::Land::LAND_TEXTURE_SIZE; + } + if (y >= ESM::Land::LAND_TEXTURE_SIZE) + { + cellY++; + y -= ESM::Land::LAND_TEXTURE_SIZE; + } + assert(xisDataLoaded(ESM::Land::DATA_VTEX)) + land->loadData(ESM::Land::DATA_VTEX); + + int tex = land->mLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; + if (tex == 0) + return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin + return std::make_pair(tex, land->mPlugin); + } + else + return std::make_pair(0,0); + } + + std::string Storage::getTextureName(UniqueTextureId id) + { + if (id.first == 0) + return "_land_default.dds"; // Not sure if the default texture really is hardcoded? + + // NB: All vtex ids are +1 compared to the ltex ids + const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); + + std::string texture = ltex->mTexture; + //TODO this is needed due to MWs messed up texture handling + texture = texture.substr(0, texture.rfind(".")) + ".dds"; + + return texture; + } + + void Storage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); + int cellX = origin.x; + int cellY = origin.y; + + // Save the used texture indices so we know the total number of textures + // and number of required blend maps + std::set textureIndices; + // Due to the way the blending works, the base layer will always shine through in between + // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). + // To get a consistent look, we need to make sure to use the same base layer in all cells. + // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. + textureIndices.insert(std::make_pair(0,0)); + // NB +1 to get the last index from neighbour cell (see getVtexIndexAt) + for (int y=0; y textureIndicesMap; + for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) + { + int size = textureIndicesMap.size(); + textureIndicesMap[*it] = size; + layerList.push_back(getTextureName(*it)); + } + + int numTextures = textureIndices.size(); + // numTextures-1 since the base layer doesn't need blending + int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); + + int channels = pack ? 4 : 1; + + // Second iteration - create and fill in the blend maps + const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; + std::vector data; + data.resize(blendmapSize * blendmapSize * channels, 0); + + for (int i=0; isecond; + int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); + int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; + + if (blendIndex == i) + data[y*blendmapSize*channels + x*channels + channel] = 255; + else + data[y*blendmapSize*channels + x*channels + channel] = 0; + } + } + + // All done, upload to GPU + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); + map->loadRawData(stream, blendmapSize, blendmapSize, format); + blendmaps.push_back(map); + } + } + + +} diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp new file mode 100644 index 000000000..d55403d9c --- /dev/null +++ b/components/terrain/storage.hpp @@ -0,0 +1,76 @@ +#ifndef COMPONENTS_TERRAIN_STORAGE_H +#define COMPONENTS_TERRAIN_STORAGE_H + +#include +#include + +#include + +#include + +namespace Terrain +{ + + /// We keep storage of terrain data abstract here since we need different implementations for game and editor + class Storage + { + private: + virtual ESM::Land* getLand (int cellX, int cellY) = 0; + virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0; + + public: + /// Get bounds of the whole terrain in cell units + virtual Ogre::AxisAlignedBox getBounds() = 0; + + /// Get the minimum and maximum heights of a terrain chunk. + /// @note Should only be called for chunks <= 1 cell, i.e. leafs of the quad tree. + /// Larger chunks can simply merge AABB of children. + /// @param size size of the chunk in cell units + /// @param center center of the chunk in cell units + /// @param min min height will be stored here + /// @param max max height will be stored here + /// @return true if there was data available for this terrain chunk + bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); + + /// Fill vertex buffers for a terrain chunk. + /// @param lodLevel LOD level, 0 = most detailed + /// @param size size of the terrain chunk in cell units + /// @param center center of the chunk in cell units + /// @param vertexBuffer buffer to write vertices + /// @param normalBuffer buffer to write vertex normals + /// @param colourBuffer buffer to write vertex colours + void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, + Ogre::HardwareVertexBufferSharedPtr vertexBuffer, + Ogre::HardwareVertexBufferSharedPtr normalBuffer, + Ogre::HardwareVertexBufferSharedPtr colourBuffer); + + /// Create textures holding layer blend values for a terrain chunk. + /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @param chunkSize size of the terrain chunk in cell units + /// @param chunkCenter center of the chunk in cell units + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + /// @param blendmaps created blendmaps will be written here + /// @param layerList names of the layer textures used will be written here + void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + + private: + void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + + // Since plugins can define new texture palettes, we need to know the plugin index too + // in order to retrieve the correct texture name. + // pair + typedef std::pair UniqueTextureId; + + UniqueTextureId getVtexIndexAt(int cellX, int cellY, + int x, int y); + std::string getTextureName (UniqueTextureId id); + }; + +} + +#endif diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp new file mode 100644 index 000000000..c0f6cf2eb --- /dev/null +++ b/components/terrain/terrain.cpp @@ -0,0 +1,359 @@ +#include "terrain.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "storage.hpp" +#include "quadtreenode.hpp" + +namespace +{ + + bool isPowerOfTwo(int x) + { + return ( (x > 0) && ((x & (x - 1)) == 0) ); + } + + int nextPowerOfTwo (int v) + { + if (isPowerOfTwo(v)) return v; + int depth=0; + while(v) + { + v >>= 1; + depth++; + } + return 1 << depth; + } + + Terrain::QuadTreeNode* findNode (const Ogre::Vector2& center, Terrain::QuadTreeNode* node) + { + if (center == node->getCenter()) + return node; + + if (center.x > node->getCenter().x && center.y > node->getCenter().y) + return findNode(center, node->getChild(Terrain::NE)); + else if (center.x > node->getCenter().x && center.y < node->getCenter().y) + return findNode(center, node->getChild(Terrain::SE)); + else if (center.x < node->getCenter().x && center.y > node->getCenter().y) + return findNode(center, node->getChild(Terrain::NW)); + else //if (center.x < node->getCenter().x && center.y < node->getCenter().y) + return findNode(center, node->getChild(Terrain::SW)); + } + +} + +namespace Terrain +{ + + Terrain::Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags) + : mStorage(storage) + , mMinBatchSize(1) + , mMaxBatchSize(64) + , mSceneMgr(sceneMgr) + , mVisibilityFlags(visibilityFlags) + { + mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); + + Ogre::Camera* compositeMapCam = mCompositeMapSceneMgr->createCamera("a"); + mCompositeMapRenderTexture = Ogre::TextureManager::getSingleton().createManual( + "terrain/comp/rt", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, 128, 128, 0, Ogre::PF_A8B8G8R8, Ogre::TU_RENDERTARGET); + mCompositeMapRenderTarget = mCompositeMapRenderTexture->getBuffer()->getRenderTarget(); + mCompositeMapRenderTarget->setAutoUpdated(false); + mCompositeMapRenderTarget->addViewport(compositeMapCam); + + mBounds = storage->getBounds(); + + int origSizeX = mBounds.getSize().x; + int origSizeY = mBounds.getSize().y; + + // Dividing a quad tree only works well for powers of two, so round up to the nearest one + int size = nextPowerOfTwo(std::max(origSizeX, origSizeY)); + + // Adjust the center according to the new size + Ogre::Vector3 center = mBounds.getCenter() + Ogre::Vector3((size-origSizeX)/2.f, (size-origSizeY)/2.f, 0); + + mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(center.x, center.y), NULL); + buildQuadTree(mRootNode); + } + + Terrain::~Terrain() + { + delete mRootNode; + delete mStorage; + } + + void Terrain::buildQuadTree(QuadTreeNode *node) + { + float halfSize = node->getSize()/2.f; + + if (node->getSize() <= mMinBatchSize) + { + // We arrived at a leaf + float minZ,maxZ; + Ogre::Vector2 center = node->getCenter(); + if (mStorage->getMinMaxHeights(node->getSize(), center, minZ, maxZ)) + node->setBoundingBox(Ogre::AxisAlignedBox(Ogre::Vector3(-halfSize*8192, -halfSize*8192, minZ), + Ogre::Vector3(halfSize*8192, halfSize*8192, maxZ))); + else + node->markAsDummy(); // no data available for this node, skip it + return; + } + + if (node->getCenter().x - halfSize > mBounds.getMaximum().x + || node->getCenter().x + halfSize < mBounds.getMinimum().x + || node->getCenter().y - halfSize > mBounds.getMaximum().y + || node->getCenter().y + halfSize < mBounds.getMinimum().y ) + // Out of bounds of the actual terrain - this will happen because + // we rounded the size up to the next power of two + { + node->markAsDummy(); + return; + } + + // Not a leaf, create its children + node->createChild(SW, halfSize, node->getCenter() - halfSize/2.f); + node->createChild(SE, halfSize, node->getCenter() + Ogre::Vector2(halfSize/2.f, -halfSize/2.f)); + node->createChild(NW, halfSize, node->getCenter() + Ogre::Vector2(-halfSize/2.f, halfSize/2.f)); + node->createChild(NE, halfSize, node->getCenter() + halfSize/2.f); + buildQuadTree(node->getChild(SW)); + buildQuadTree(node->getChild(SE)); + buildQuadTree(node->getChild(NW)); + buildQuadTree(node->getChild(NE)); + + // if all children are dummy, we are also dummy + for (int i=0; i<4; ++i) + { + if (!node->getChild((ChildDirection)i)->isDummy()) + return; + } + node->markAsDummy(); + } + + void Terrain::update(Ogre::Camera *camera) + { + mRootNode->update(camera->getRealPosition()); + mRootNode->updateIndexBuffers(); + } + + Ogre::AxisAlignedBox Terrain::getWorldBoundingBox (const Ogre::Vector2& center) + { + QuadTreeNode* node = findNode(center, mRootNode); + Ogre::AxisAlignedBox box = node->getBoundingBox(); + box.setExtents(box.getMinimum() + Ogre::Vector3(center.x, center.y, 0) * 8192, + box.getMaximum() + Ogre::Vector3(center.x, center.y, 0) * 8192); + return box; + } + + Ogre::HardwareVertexBufferSharedPtr Terrain::getVertexBuffer(int numVertsOneSide) + { + if (mUvBufferMap.find(numVertsOneSide) != mUvBufferMap.end()) + { + return mUvBufferMap[numVertsOneSide]; + } + + int vertexCount = numVertsOneSide * numVertsOneSide; + + std::vector uvs; + uvs.reserve(vertexCount*2); + + for (int col = 0; col < numVertsOneSide; ++col) + { + for (int row = 0; row < numVertsOneSide; ++row) + { + uvs.push_back(col / static_cast(numVertsOneSide-1)); // U + uvs.push_back(row / static_cast(numVertsOneSide-1)); // V + } + } + + Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr(); + Ogre::HardwareVertexBufferSharedPtr buffer = mgr->createVertexBuffer( + Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT2), + vertexCount, Ogre::HardwareBuffer::HBU_STATIC); + + buffer->writeData(0, buffer->getSizeInBytes(), &uvs[0], true); + + mUvBufferMap[numVertsOneSide] = buffer; + return buffer; + } + + Ogre::HardwareIndexBufferSharedPtr Terrain::getIndexBuffer(int flags, size_t& numIndices) + { + if (mIndexBufferMap.find(flags) != mIndexBufferMap.end()) + { + numIndices = mIndexBufferMap[flags]->getNumIndexes(); + return mIndexBufferMap[flags]; + } + + // LOD level n means every 2^n-th vertex is kept + size_t lodLevel = (flags >> (4*4)); + + size_t lodDeltas[4]; + for (int i=0; i<4; ++i) + lodDeltas[i] = (flags >> (4*i)) & (0xf); + + bool anyDeltas = (lodDeltas[North] || lodDeltas[South] || lodDeltas[West] || lodDeltas[East]); + + size_t increment = std::pow(2, lodLevel); + assert((int)increment < ESM::Land::LAND_SIZE); + std::vector indices; + indices.reserve((ESM::Land::LAND_SIZE-1)*(ESM::Land::LAND_SIZE-1)*2*3 / increment); + + size_t rowStart = 0, colStart = 0, rowEnd = ESM::Land::LAND_SIZE-1, colEnd = ESM::Land::LAND_SIZE-1; + // If any edge needs stitching we'll skip all edges at this point, + // mainly because stitching one edge would have an effect on corners and on the adjacent edges + if (anyDeltas) + { + colStart += increment; + colEnd -= increment; + rowEnd -= increment; + rowStart += increment; + } + for (size_t row = rowStart; row < rowEnd; row += increment) + { + for (size_t col = colStart; col < colEnd; col += increment) + { + indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row); + indices.push_back(ESM::Land::LAND_SIZE*col+row+increment); + + indices.push_back(ESM::Land::LAND_SIZE*col+row+increment); + indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row+increment); + } + } + + size_t innerStep = increment; + if (anyDeltas) + { + // Now configure LOD transitions at the edges - this is pretty tedious, + // and some very long and boring code, but it works great + + // South + size_t row = 0; + size_t outerStep = std::pow(2, lodDeltas[South] + lodLevel); + for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep) + { + indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); + // Make sure not to touch the left edge + if (col == 0) + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep); + else + indices.push_back(ESM::Land::LAND_SIZE*col+row+innerStep); + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the left or right edges + if (col+i == 0 || col+i == ESM::Land::LAND_SIZE-1-innerStep) + continue; + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row+innerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row+innerStep); + } + } + + // North + row = ESM::Land::LAND_SIZE-1; + outerStep = std::pow(2, lodDeltas[North] + lodLevel); + for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep) + { + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); + indices.push_back(ESM::Land::LAND_SIZE*col+row); + // Make sure not to touch the right edge + if (col+outerStep == ESM::Land::LAND_SIZE-1) + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep-innerStep)+row-innerStep); + else + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row-innerStep); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the left or right edges + if (col+i == 0 || col+i == ESM::Land::LAND_SIZE-1-innerStep) + continue; + indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row-innerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row-innerStep); + indices.push_back(ESM::Land::LAND_SIZE*col+row); + } + } + + // West + size_t col = 0; + outerStep = std::pow(2, lodDeltas[West] + lodLevel); + for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep) + { + indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); + indices.push_back(ESM::Land::LAND_SIZE*col+row); + // Make sure not to touch the bottom edge + if (row == 0) + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep); + else + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the top or bottom edges + if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep) + continue; + indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i); + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i+innerStep); + } + } + + // East + col = ESM::Land::LAND_SIZE-1; + outerStep = std::pow(2, lodDeltas[East] + lodLevel); + for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep) + { + indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); + // Make sure not to touch the top edge + if (row+outerStep == ESM::Land::LAND_SIZE-1) + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep-innerStep); + else + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the top or bottom edges + if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep) + continue; + indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i+innerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i); + } + } + } + + + + numIndices = indices.size(); + + Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr(); + Ogre::HardwareIndexBufferSharedPtr buffer = mgr->createIndexBuffer(Ogre::HardwareIndexBuffer::IT_16BIT, + numIndices, Ogre::HardwareBuffer::HBU_STATIC); + buffer->writeData(0, buffer->getSizeInBytes(), &indices[0], true); + mIndexBufferMap[flags] = buffer; + return buffer; + } + + void Terrain::renderCompositeMap(Ogre::TexturePtr target) + { + mCompositeMapRenderTarget->update(); + target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer()); + } + + void Terrain::clearCompositeMapSceneManager() + { + mCompositeMapSceneMgr->destroyAllManualObjects(); + mCompositeMapSceneMgr->clearScene(); + } + + +} diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp new file mode 100644 index 000000000..79e35fd8e --- /dev/null +++ b/components/terrain/terrain.hpp @@ -0,0 +1,121 @@ +#ifndef COMPONENTS_TERRAIN_H +#define COMPONENTS_TERRAIN_H + +#include +#include +#include +#include + +namespace Ogre +{ + class Camera; +} + +namespace Terrain +{ + + class QuadTreeNode; + class Storage; + + /** + * @brief A quadtree-based terrain implementation suitable for large data sets. \n + * Near cells are rendered with alpha splatting, distant cells are merged + * together in batches and have their layers pre-rendered onto a composite map. \n + * Cracks at LOD transitions are avoided using stitching. + * @note Multiple cameras are not supported yet + */ + class Terrain + { + public: + /// @note takes ownership of \a storage + Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags); + ~Terrain(); + + /// Update chunk LODs according to this camera position + /// @note Calling this method might lead to composite textures being rendered, so it is best + /// not to call it when render commands are still queued, since that would cause a flush. + void update (Ogre::Camera* camera); + + /// \todo + float getHeightAt (const Ogre::Vector3& worldPos) { return 0; } + + /// Get the world bounding box of a chunk of terrain centered at \a center + Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); + + Ogre::SceneManager* getSceneManager() { return mSceneMgr; } + + Storage* getStorage() { return mStorage; } + + /// Show or hide the whole terrain + void setVisible(bool visible); + + /// Recreate materials used by terrain chunks. This should be called whenever settings of + /// the material factory are changed. (Relying on the factory to update those materials is not + /// enough, since turning a feature on/off can change the number of texture units available for layer/blend + /// textures, and to properly respond to this we may need to change the structure of the material, such as + /// adding or removing passes. This can only be achieved by a full rebuild.) + void applyMaterials(); + + int getVisiblityFlags() { return mVisibilityFlags; } + + int getMaxBatchSize() { return mMaxBatchSize; } + + void enableSplattingShader(bool enabled); + + private: + QuadTreeNode* mRootNode; + Storage* mStorage; + + int mVisibilityFlags; + + Ogre::SceneManager* mSceneMgr; + Ogre::SceneManager* mCompositeMapSceneMgr; + + /// Bounds in cell units + Ogre::AxisAlignedBox mBounds; + + /// Minimum size of a terrain batch along one side (in cell units) + float mMinBatchSize; + /// Maximum size of a terrain batch along one side (in cell units) + float mMaxBatchSize; + + void buildQuadTree(QuadTreeNode* node); + + public: + // ----INTERNAL---- + + enum IndexBufferFlags + { + IBF_North = 1 << 0, + IBF_East = 1 << 1, + IBF_South = 1 << 2, + IBF_West = 1 << 3 + }; + + /// @param flags first 4*4 bits are LOD deltas on each edge, respectively (4 bits each) + /// next 4 bits are LOD level of the index buffer (LOD 0 = don't omit any vertices) + /// @param numIndices number of indices that were used will be written here + Ogre::HardwareIndexBufferSharedPtr getIndexBuffer (int flags, size_t& numIndices); + + Ogre::HardwareVertexBufferSharedPtr getVertexBuffer (int numVertsOneSide); + + Ogre::SceneManager* getCompositeMapSceneManager() { return mCompositeMapSceneMgr; } + + // Delete all quads + void clearCompositeMapSceneManager(); + void renderCompositeMap (Ogre::TexturePtr target); + + private: + // Index buffers are shared across terrain batches where possible. There is one index buffer for each + // combination of LOD deltas and index buffer LOD we may need. + std::map mIndexBufferMap; + + std::map mUvBufferMap; + + Ogre::RenderTarget* mCompositeMapRenderTarget; + Ogre::TexturePtr mCompositeMapRenderTexture; + }; + +} + +#endif diff --git a/files/materials/objects.shader b/files/materials/objects.shader index 0c869d2cb..36f92bfd9 100644 --- a/files/materials/objects.shader +++ b/files/materials/objects.shader @@ -80,7 +80,6 @@ #endif #if VERTEX_LIGHTING - shUniform(float, lightCount) @shAutoConstant(lightCount, light_count) shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_view_space_array, @shGlobalSettingString(num_lights)) shUniform(float4, lightDiffuse[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightDiffuse, light_diffuse_colour_array, @shGlobalSettingString(num_lights)) shUniform(float4, lightAttenuation[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightAttenuation, light_attenuation_array, @shGlobalSettingString(num_lights)) diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index 133022957..1f11217d2 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -2,7 +2,7 @@ #define IS_FIRST_PASS (@shPropertyString(pass_index) == 0) -#define FOG @shGlobalSettingBool(fog) +#define FOG (@shGlobalSettingBool(fog) && !@shPropertyBool(render_composite_map)) #define SHADOWS_PSSM @shGlobalSettingBool(shadows_pssm) #define SHADOWS @shGlobalSettingBool(shadows) @@ -11,8 +11,6 @@ #include "shadows.h" #endif -#define COLOUR_MAP @shPropertyBool(colour_map) - #define NUM_LAYERS @shPropertyString(num_layers) #if FOG || SHADOWS_PSSM @@ -23,9 +21,12 @@ #define VIEWPROJ_FIX @shGlobalSettingBool(viewproj_fix) -#if !IS_FIRST_PASS -// This is not the first pass. -#endif +#define RENDERCMP @shPropertyBool(render_composite_map) + +#define LIGHTING !RENDERCMP + +#define COMPOSITE_MAP @shPropertyBool(display_composite_map) + #if NEED_DEPTH @shAllocatePassthrough(1, depth) @@ -35,6 +36,11 @@ @shAllocatePassthrough(3, worldPos) +#if LIGHTING +@shAllocatePassthrough(3, lightResult) +@shAllocatePassthrough(3, directionalResult) +#endif + #if SHADOWS @shAllocatePassthrough(4, lightSpacePos0) #endif @@ -55,11 +61,19 @@ #if VIEWPROJ_FIX shUniform(float4, vpRow2Fix) @shSharedParameter(vpRow2Fix, vpRow2Fix) #endif - - shUniform(float2, lodMorph) @shAutoConstant(lodMorph, custom, 1001) shVertexInput(float2, uv0) shVertexInput(float2, uv1) // lodDelta, lodThreshold + +#if LIGHTING + shNormalInput(float4) + shColourInput(float4) + + shUniform(float, lightCount) @shAutoConstant(lightCount, light_count) + shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_object_space_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightDiffuse[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightDiffuse, light_diffuse_colour_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightAttenuation[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightAttenuation, light_attenuation_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightAmbient) @shAutoConstant(lightAmbient, ambient_light_colour) #if SHADOWS shUniform(float4x4, texViewProjMatrix0) @shAutoConstant(texViewProjMatrix0, texture_viewproj_matrix) @@ -71,31 +85,15 @@ @shEndForeach #endif +#endif + @shPassthroughVertexOutputs SH_START_PROGRAM { - - float4 worldPos = shMatrixMult(worldMatrix, shInputPosition); - // determine whether to apply the LOD morph to this vertex - // we store the deltas against all vertices so we only want to apply - // the morph to the ones which would disappear. The target LOD which is - // being morphed to is stored in lodMorph.y, and the LOD at which - // the vertex should be morphed is stored in uv.w. If we subtract - // the former from the latter, and arrange to only morph if the - // result is negative (it will only be -1 in fact, since after that - // the vertex will never be indexed), we will achieve our aim. - // sign(vertexLOD - targetLOD) == -1 is to morph - float toMorph = -min(0, sign(uv1.y - lodMorph.y)); - - // morph - // this assumes XY terrain alignment - worldPos.z += uv1.x * toMorph * lodMorph.x; - - shOutputPosition = shMatrixMult(viewProjMatrix, worldPos); #if NEED_DEPTH @@ -124,6 +122,8 @@ @shPassthroughAssign(worldPos, worldPos.xyz); +#if LIGHTING + #if SHADOWS float4 lightSpacePos = shMatrixMult(texViewProjMatrix0, shMatrixMult(worldMatrix, shInputPosition)); @shPassthroughAssign(lightSpacePos0, lightSpacePos); @@ -138,6 +138,33 @@ @shEndForeach #endif + + // Lighting + float3 lightDir; + float d; + float3 lightResult = float3(0,0,0); + float3 directionalResult = float3(0,0,0); + @shForeach(@shGlobalSettingString(num_lights)) + lightDir = lightPosition[@shIterator].xyz - (shInputPosition.xyz * lightPosition[@shIterator].w); + d = length(lightDir); + lightDir = normalize(lightDir); + + + lightResult.xyz += lightDiffuse[@shIterator].xyz + * shSaturate(1.0 / ((lightAttenuation[@shIterator].y) + (lightAttenuation[@shIterator].z * d) + (lightAttenuation[@shIterator].w * d * d))) + * max(dot(normal.xyz, lightDir), 0); + +#if @shIterator == 0 + directionalResult = lightResult.xyz; +#endif + @shEndForeach + lightResult.xyz += lightAmbient.xyz; + lightResult.xyz *= colour.xyz; + + @shPassthroughAssign(lightResult, lightResult); + @shPassthroughAssign(directionalResult, directionalResult); + +#endif } #else @@ -151,12 +178,9 @@ SH_BEGIN_PROGRAM -#if COLOUR_MAP - shSampler2D(colourMap) -#endif - - shSampler2D(normalMap) // global normal map - +#if COMPOSITE_MAP + shSampler2D(compositeMap) +#else @shForeach(@shPropertyString(num_blendmaps)) shSampler2D(blendMap@shIterator) @@ -165,6 +189,8 @@ @shForeach(@shPropertyString(num_layers)) shSampler2D(diffuseMap@shIterator) @shEndForeach + +#endif #if FOG shUniform(float3, fogColour) @shAutoConstant(fogColour, fog_colour) @@ -215,9 +241,6 @@ float2 UV = @shPassthroughReceive(UV); float3 worldPos = @shPassthroughReceive(worldPos); - - float3 normal = shSample(normalMap, UV).rgb * 2 - 1; - normal = normalize(normal); #if UNDERWATER @@ -230,17 +253,26 @@ float previousAlpha = 1.f; #endif + +shOutputColour(0) = float4(1,1,1,1); + +#if COMPOSITE_MAP + shOutputColour(0).xyz = shSample(compositeMap, UV).xyz; +#else + // Layer calculations -// rescale UV to directly map vertices to texel centers +// rescale UV to directly map edge vertices to texel centers - this is +// important to get correct blending at cell transitions // TODO: parameterize texel size -float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; +float2 blendUV = (UV - 0.5) * (16.0 / (16.0+1.0)) + 0.5; @shForeach(@shPropertyString(num_blendmaps)) - float4 blendValues@shIterator = shSample(blendMap@shIterator, blendUV); + float4 blendValues@shIterator = shSaturate(shSample(blendMap@shIterator, blendUV)); @shEndForeach + float3 albedo = float3(0,0,0); - float2 layerUV = UV * 8; + float2 layerUV = UV * 16; @shForeach(@shPropertyString(num_layers)) @@ -262,25 +294,14 @@ float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; #endif @shEndForeach - shOutputColour(0) = float4(1,1,1,1); - + shOutputColour(0).rgb *= albedo; -#if COLOUR_MAP - // Since we're emulating vertex colors here, - // rescale UV to directly map vertices to texel centers. TODO: parameterize texel size - const float colourmapSize = 33.f; - float2 colourUV = (UV - 0.5) * (colourmapSize / (colourmapSize+1.f)) + 0.5; - shOutputColour(0).rgb *= shSample(colourMap, colourUV).rgb; #endif - shOutputColour(0).rgb *= albedo; - - - - - - +#if LIGHTING // Lighting + float3 lightResult = @shPassthroughReceive(lightResult); + float3 directionalResult = @shPassthroughReceive(directionalResult); // shadows only for the first (directional) light #if SHADOWS @@ -305,40 +326,9 @@ float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; float shadow = 1.0; #endif - - - float3 lightDir; - float3 diffuse = float3(0,0,0); - float d; - - @shForeach(@shGlobalSettingString(terrain_num_lights)) - - lightDir = lightPosObjSpace@shIterator.xyz - (worldPos.xyz * lightPosObjSpace@shIterator.w); - d = length(lightDir); - - - lightDir = normalize(lightDir); - -#if @shIterator == 0 - - #if (SHADOWS || SHADOWS_PSSM) - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0) * shadow; - - #else - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0); - - #endif - -#else - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0); + shOutputColour(0).xyz *= (lightResult - directionalResult * (1.0-shadow)); #endif - @shEndForeach - - shOutputColour(0).xyz *= (lightAmbient.xyz + diffuse); - - - #if FOG float fogValue = shSaturate((depth - fogParams.y) * fogParams.w); diff --git a/libs/openengine/ogre/imagerotate.cpp b/libs/openengine/ogre/imagerotate.cpp index 3dd584078..9c32924f1 100644 --- a/libs/openengine/ogre/imagerotate.cpp +++ b/libs/openengine/ogre/imagerotate.cpp @@ -56,7 +56,7 @@ void ImageRotate::rotate(const std::string& sourceImage, const std::string& dest TEX_TYPE_2D, width, height, 0, - PF_FLOAT16_RGBA, + PF_A8B8G8R8, TU_RENDERTARGET); RenderTarget* rtt = destTextureRot->getBuffer()->getRenderTarget(); @@ -75,7 +75,7 @@ void ImageRotate::rotate(const std::string& sourceImage, const std::string& dest TEX_TYPE_2D, width, height, 0, - PF_FLOAT16_RGBA, + PF_A8B8G8R8, Ogre::TU_STATIC); destTexture->getBuffer()->blit(destTextureRot->getBuffer()); From 584eec37434409f982150c3d60d88f2705b953b5 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Fri, 16 Aug 2013 04:18:48 -0700 Subject: [PATCH 034/194] Store the object class in the LiveCellRef --- apps/openmw/mwworld/class.cpp | 5 +++-- apps/openmw/mwworld/class.hpp | 8 +++++++- apps/openmw/mwworld/livecellref.hpp | 7 +++---- apps/openmw/mwworld/ptr.cpp | 15 +++++++++++++++ apps/openmw/mwworld/ptr.hpp | 10 ++++++---- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index c7b8d341c..c739ea831 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -245,9 +245,10 @@ namespace MWWorld throw std::runtime_error ("class does not support persistence"); } - void Class::registerClass (const std::string& key, boost::shared_ptr instance) + void Class::registerClass(const std::string& key, boost::shared_ptr instance) { - sClasses.insert (std::make_pair (key, instance)); + instance->mTypeName = key; + sClasses.insert(std::make_pair(key, instance)); } std::string Class::getUpSoundId (const Ptr& ptr) const diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index c91000052..28e37cbf3 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -50,6 +50,8 @@ namespace MWWorld { static std::map > sClasses; + std::string mTypeName; + // not implemented Class (const Class&); Class& operator= (const Class&); @@ -73,6 +75,10 @@ namespace MWWorld virtual ~Class(); + const std::string& getTypeName() const { + return mTypeName; + } + virtual std::string getId (const Ptr& ptr) const; ///< Return ID of \a ptr or throw an exception, if class does not support ID retrieval /// (default implementation: throw an exception) @@ -292,7 +298,7 @@ namespace MWWorld static const Class& get (const Ptr& ptr) { - return get(ptr.getTypeName()); + return ptr.getClass(); } ///< If there is no class for this pointer, an exception is thrown. diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 64462cb3f..415351e78 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -11,11 +11,12 @@ namespace MWWorld { class Ptr; class ESMStore; + class Class; /// Used to create pointers to hold any type of LiveCellRef<> object. struct LiveCellRefBase { - std::string mTypeName; + const Class *mClass; /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. @@ -25,9 +26,7 @@ namespace MWWorld /** runtime-data */ RefData mData; - LiveCellRefBase(std::string type, const ESM::CellRef &cref=ESM::CellRef()) - : mTypeName(type), mRef(cref), mData(mRef) - { } + LiveCellRefBase(std::string type, const ESM::CellRef &cref=ESM::CellRef()); /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase() { } }; diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp index 34fe7fa08..127ab1364 100644 --- a/apps/openmw/mwworld/ptr.cpp +++ b/apps/openmw/mwworld/ptr.cpp @@ -4,8 +4,23 @@ #include #include "containerstore.hpp" +#include "class.hpp" +/* This shouldn't really be here. */ +MWWorld::LiveCellRefBase::LiveCellRefBase(std::string type, const ESM::CellRef &cref) + : mClass(&Class::get(type)), mRef(cref), mData(mRef) +{ +} + + +const std::string& MWWorld::Ptr::getTypeName() const +{ + if(mRef != 0) + return mRef->mClass->getTypeName(); + throw std::runtime_error("Can't get type name from an empty object."); +} + ESM::CellRef& MWWorld::Ptr::getCellRef() const { assert(mRef); diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 51c1530d7..e5352da28 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -32,11 +32,13 @@ namespace MWWorld return mRef == 0; } - const std::string& getTypeName() const + const std::string& getTypeName() const; + + const Class& getClass() const { if(mRef != 0) - return mRef->mTypeName; - throw std::runtime_error("Can't get type name from an empty object."); + return *(mRef->mClass); + throw std::runtime_error("Cannot get class of an empty object"); } template @@ -47,7 +49,7 @@ namespace MWWorld std::stringstream str; str<< "Bad LiveCellRef cast to "<mTypeName; + if(mRef != 0) str<< getTypeName(); else str<< "an empty object"; throw std::runtime_error(str.str()); From 0076c558d61da4080b807a336318dacb9021d9dc Mon Sep 17 00:00:00 2001 From: eroen Date: Fri, 16 Aug 2013 22:32:16 +0200 Subject: [PATCH 035/194] Re-introduce lost functionality The branch merged in 5a863589b4 removed fine-grained configure-time control over install paths. This is necessary to accomodate various linux distros' policies, eg. Gentoo wants games installed in /usr/games, but with resource files in /usr/share/games. DOCDIR and MANDIR appear to be unused, and were not re-introduced. --- CMakeLists.txt | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 575fecd0c..85860609b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -334,12 +334,18 @@ IF(NOT WIN32 AND NOT APPLE) IF (DPKG_PROGRAM) ## Debian specific SET(CMAKE_INSTALL_PREFIX "/usr") + SET(DATAROOTDIR "share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(DATADIR "share/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "share/pixmaps" CACHE PATH "Set icon dir") SET(SYSCONFDIR "../etc/openmw" CACHE PATH "Set config dir") ELSE () ## Non debian specific SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") - SET(LICDIR "${CMAKE_INSTALL_PREFIX}/share/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") + SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") # Install binaries INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) @@ -367,11 +373,11 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF (DPKG_PROGRAM) # Install icon and desktop file - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "share/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "share/pixmaps/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "share/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "share/pixmaps/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files @@ -383,8 +389,8 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF(BUILD_OPENCS) # Install resources - INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "share/games/openmw/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") - INSTALL(DIRECTORY DESTINATION "share/games/openmw/data/" COMPONENT "Resources") + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") + INSTALL(DIRECTORY DESTINATION "${DATADIR}/data/" COMPONENT "Resources") IF (DPKG_PROGRAM) ## Debian Specific From 8d925b7fd640b75bae682ba6bda9b16dcd371ed0 Mon Sep 17 00:00:00 2001 From: eroen Date: Fri, 16 Aug 2013 22:32:16 +0200 Subject: [PATCH 036/194] cleanup - drop trailing slashes from paths for consistency - sort entries that got unsorted --- CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85860609b..dd58ed416 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -340,12 +340,12 @@ IF(NOT WIN32 AND NOT APPLE) SET(SYSCONFDIR "../etc/openmw" CACHE PATH "Set config dir") ELSE () ## Non debian specific - SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") # Install binaries INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) @@ -373,11 +373,11 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF (DPKG_PROGRAM) # Install icon and desktop file - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}/" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files @@ -389,8 +389,8 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF(BUILD_OPENCS) # Install resources - INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}/" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") - INSTALL(DIRECTORY DESTINATION "${DATADIR}/data/" COMPONENT "Resources") + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") + INSTALL(DIRECTORY DESTINATION "${DATADIR}/data" COMPONENT "Resources") IF (DPKG_PROGRAM) ## Debian Specific From 76b812f75fa1c6acab36605ac9359ce37869a23d Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 01:11:57 -0700 Subject: [PATCH 037/194] Improve actor movement collision handling --- apps/openmw/mwworld/physicssystem.cpp | 127 ++++++++++++-------------- 1 file changed, 56 insertions(+), 71 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 371783bc5..e8450e1d2 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -31,17 +31,22 @@ namespace MWWorld static const float sMaxSlope = 60.0f; static const float sStepSize = 30.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. - static const int sMaxIterations = 50; + static const int sMaxIterations = 4; class MovementSolver { private: - static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, const Ogre::Vector3 &velocity, float remainingTime, + static float getSlope(const Ogre::Vector3 &normal) + { + return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); + } + + static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, + const Ogre::Vector3 &velocity, float &remainingTime, const Ogre::Vector3 &halfExtents, bool isInterior, OEngine::Physic::PhysicEngine *engine) { - traceResults trace; // no initialization needed - + traceResults trace; newtrace(&trace, orient, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); if(trace.fraction == 0.0f) @@ -51,45 +56,34 @@ namespace MWWorld halfExtents, isInterior, engine); if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) return false; + float movefrac = trace.fraction; newtrace(&trace, orient, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); if(getSlope(trace.planenormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. position = trace.endpos; + remainingTime *= (1.0f-movefrac); return true; } return false; } - static void clipVelocity(Ogre::Vector3& inout, const Ogre::Vector3& normal, float overbounce=1.0f) - { - //Math stuff. Basically just project the velocity vector onto the plane represented by the normal. - //More specifically, it projects velocity onto the normal, takes that result, multiplies it by overbounce and then subtracts it from velocity. - float backoff = inout.dotProduct(normal); - if(backoff < 0.0f) - backoff *= overbounce; - else - backoff /= overbounce; - - inout -= normal*backoff; - } - static void projectVelocity(Ogre::Vector3& velocity, const Ogre::Vector3& direction) + ///Project a vector u on another vector v + static inline Ogre::Vector3 project(const Ogre::Vector3 u, const Ogre::Vector3 &v) { - Ogre::Vector3 normalizedDirection(direction); - normalizedDirection.normalise(); - - // no divide by normalizedDirection.length necessary because it's normalized - velocity = normalizedDirection * velocity.dotProduct(normalizedDirection); + return v * u.dotProduct(v); } - static float getSlope(const Ogre::Vector3 &normal) + ///Helper for computing the character sliding + static inline Ogre::Vector3 slide(Ogre::Vector3 direction, const Ogre::Vector3 &planeNormal) { - return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); + return direction - project(direction, planeNormal); } + public: static Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, OEngine::Physic::PhysicEngine *engine) { @@ -104,7 +98,6 @@ namespace MWWorld return position; bool wasCollisionMode = physicActor->getCollisionMode(); - physicActor->enableCollisions(false); Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); @@ -124,10 +117,9 @@ namespace MWWorld if (wasCollisionMode) physicActor->enableCollisions(true); - if (hit) - return newPosition+Ogre::Vector3(0,0,4); - else + if(!hit) return position; + return newPosition+Ogre::Vector3(0,0,4); } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, @@ -147,14 +139,13 @@ namespace MWWorld movement; } - traceResults trace; //no initialization needed - bool onground = false; - float remainingTime = time; bool isInterior = !ptr.getCell()->isExterior(); Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); - bool wasCollisionMode = physicActor->getCollisionMode(); physicActor->enableCollisions(false); - Ogre::Quaternion orient = Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[2]), Ogre::Vector3::UNIT_Z); + + traceResults trace; + bool onground = false; + const Ogre::Quaternion orient; // Don't rotate actor collision boxes Ogre::Vector3 velocity; if(!gravity) { @@ -175,53 +166,46 @@ namespace MWWorld velocity.z += physicActor->getVerticalForce(); } - Ogre::Vector3 clippedVelocity(velocity); if(onground) { - // if we're on the ground, force velocity to track it - clippedVelocity.z = velocity.z = std::max(0.0f, velocity.z); - clipVelocity(clippedVelocity, trace.planenormal); + // if we're on the ground, don't try to fall + velocity.z = std::max(0.0f, velocity.z); } const Ogre::Vector3 up(0.0f, 0.0f, 1.0f); Ogre::Vector3 newPosition = position; - int iterations = 0; - do { + float remainingTime = time; + for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) + { // trace to where character would go if there were no obstructions - newtrace(&trace, orient, newPosition, newPosition+clippedVelocity*remainingTime, halfExtents, isInterior, engine); - newPosition = trace.endpos; - remainingTime = remainingTime * (1.0f-trace.fraction); + newtrace(&trace, orient, newPosition, newPosition+velocity*remainingTime, halfExtents, isInterior, engine); // check for obstructions - if(trace.fraction < 1.0f) + if(trace.fraction >= 1.0f) { - //std::cout<<"angle: "< 0.0f); + //std::cout<<"angle: "<setOnGround(onground); - physicActor->setVerticalForce(!onground ? clippedVelocity.z - time*627.2f : 0.0f); - if (wasCollisionMode) - physicActor->enableCollisions(true); + physicActor->setVerticalForce(!onground ? velocity.z - time*627.2f : 0.0f); + physicActor->enableCollisions(true); + return newPosition; } }; From 14acacf4012b0466302bbd892585f1b3ee3114a6 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 02:51:19 -0700 Subject: [PATCH 038/194] Use a better method to do actor physics traces --- apps/openmw/mwworld/physicssystem.cpp | 35 ++++++------ libs/openengine/bullet/physic.hpp | 48 +++++++++------- libs/openengine/bullet/trace.cpp | 82 +++++++++++++++++++++++++-- libs/openengine/bullet/trace.h | 4 ++ 4 files changed, 124 insertions(+), 45 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index e8450e1d2..ea013bc7a 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -41,24 +41,21 @@ namespace MWWorld return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); } - static bool stepMove(Ogre::Vector3& position, const Ogre::Quaternion& orient, + static bool stepMove(btCollisionObject *colobj, Ogre::Vector3 &position, const Ogre::Vector3 &velocity, float &remainingTime, - const Ogre::Vector3 &halfExtents, bool isInterior, OEngine::Physic::PhysicEngine *engine) { traceResults trace; - newtrace(&trace, orient, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), - halfExtents, isInterior, engine); + actortrace(&trace, colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); if(trace.fraction == 0.0f) return false; - newtrace(&trace, orient, trace.endpos, trace.endpos + velocity*remainingTime, - halfExtents, isInterior, engine); + actortrace(&trace, colobj, trace.endpos, trace.endpos + velocity*remainingTime, engine); if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) return false; float movefrac = trace.fraction; - newtrace(&trace, orient, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), halfExtents, isInterior, engine); + actortrace(&trace, colobj, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); if(getSlope(trace.planenormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. @@ -100,7 +97,7 @@ namespace MWWorld bool wasCollisionMode = physicActor->getCollisionMode(); physicActor->enableCollisions(false); - Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); + Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); halfExtents.z = 1; // we trace the feet only, so we use a very thin box Ogre::Vector3 newPosition = position; @@ -133,15 +130,15 @@ namespace MWWorld if(!physicActor || !physicActor->getCollisionMode()) { // FIXME: This works, but it's inconcsistent with how the rotations are applied elsewhere. Why? - return position + (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* - Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* - Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * + return position + (Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)* + Ogre::Quaternion(Ogre::Radian(-refpos.rot[1]), Ogre::Vector3::UNIT_Y)* + Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * movement; } - bool isInterior = !ptr.getCell()->isExterior(); - Ogre::Vector3 halfExtents = physicActor->getHalfExtents();// + Vector3(1,1,1); - physicActor->enableCollisions(false); + btCollisionObject *colobj = physicActor->getCollisionBody(); + Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); + position.z += halfExtents.z; traceResults trace; bool onground = false; @@ -158,7 +155,7 @@ namespace MWWorld { if(!(movement.z > 0.0f)) { - newtrace(&trace, orient, position, position-Ogre::Vector3(0,0,4), halfExtents, isInterior, engine); + actortrace(&trace, colobj, position, position-Ogre::Vector3(0,0,4), engine); if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) onground = true; } @@ -178,7 +175,7 @@ namespace MWWorld for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) { // trace to where character would go if there were no obstructions - newtrace(&trace, orient, newPosition, newPosition+velocity*remainingTime, halfExtents, isInterior, engine); + actortrace(&trace, colobj, newPosition, newPosition+velocity*remainingTime, engine); // check for obstructions if(trace.fraction >= 1.0f) @@ -190,7 +187,7 @@ namespace MWWorld //std::cout<<"angle: "<setOnGround(onground); physicActor->setVerticalForce(!onground ? velocity.z - time*627.2f : 0.0f); - physicActor->enableCollisions(true); + newPosition.z -= halfExtents.z; return newPosition; } }; diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index 32af0da4e..9b1541ea2 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -66,6 +66,20 @@ namespace Physic std::string mName; }; + /** + *This class is just an extension of normal btRigidBody in order to add extra info. + *When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, + *so one never should use btRigidBody directly! + */ + class RigidBody: public btRigidBody + { + public: + RigidBody(btRigidBody::btRigidBodyConstructionInfo& CI,std::string name); + virtual ~RigidBody(); + std::string mName; + bool mPlaceable; + }; + /** * A physic actor uses a rigid body based on box shapes. * Pmove is used to move the physic actor around the dynamic world. @@ -129,47 +143,41 @@ namespace Physic bool getOnGround() const; + btCollisionObject *getCollisionBody() const + { + return mBody; + } + private: void disableCollisionBody(); void enableCollisionBody(); public: //HACK: in Visual Studio 2010 and presumably above, this structures alignment -// must be 16, but the built in operator new & delete don't properly -// perform this alignment. +// must be 16, but the built in operator new & delete don't properly +// perform this alignment. #if _MSC_VER >= 1600 - void * operator new (size_t Size) { return _aligned_malloc (Size, 16); } - void operator delete (void * Data) { _aligned_free (Data); } + void * operator new (size_t Size) { return _aligned_malloc (Size, 16); } + void operator delete (void * Data) { _aligned_free (Data); } #endif private: - OEngine::Physic::RigidBody* mBody; OEngine::Physic::RigidBody* mRaycastingBody; + Ogre::Vector3 mBoxScaledTranslation; - btQuaternion mBoxRotationInverse; Ogre::Quaternion mBoxRotation; + btQuaternion mBoxRotationInverse; + float verticalForce; bool onGround; bool collisionMode; + std::string mMesh; - PhysicEngine* mEngine; std::string mName; + PhysicEngine *mEngine; }; - /** - *This class is just an extension of normal btRigidBody in order to add extra info. - *When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, - *so one never should use btRigidBody directly! - */ - class RigidBody: public btRigidBody - { - public: - RigidBody(btRigidBody::btRigidBodyConstructionInfo& CI,std::string name); - virtual ~RigidBody(); - std::string mName; - bool mPlaceable; - }; struct HeightField { diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index d246417c7..08c42d9d0 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -8,12 +8,6 @@ #include "physic.hpp" -enum traceWorldType -{ - collisionWorldTrace = 1, - pickWorldTrace = 2, - bothWorldTrace = collisionWorldTrace | pickWorldTrace -}; void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine *enginePass) //Traceobj was a Aedra Object { @@ -46,3 +40,79 @@ void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre: results->fraction = 1.0f; } } + + +class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback +{ +public: + ClosestNotMeConvexResultCallback(btCollisionObject *me, const btVector3 &up, btScalar minSlopeDot) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), + mMe(me), mUp(up), mMinSlopeDot(minSlopeDot) + { + } + + virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) + { + if(convexResult.m_hitCollisionObject == mMe) + return btScalar( 1 ); + + btVector3 hitNormalWorld; + if(normalInWorldSpace) + hitNormalWorld = convexResult.m_hitNormalLocal; + else + { + ///need to transform normal into worldspace + hitNormalWorld = m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + } + + // NOTE : m_hitNormalLocal is not always vertical on the ground with a capsule or a box... + + btScalar dotUp = mUp.dot(hitNormalWorld); + if(dotUp < mMinSlopeDot) + return btScalar(1); + + return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); + } + +protected: + btCollisionObject *mMe; + const btVector3 mUp; + const btScalar mMinSlopeDot; +}; + +void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, OEngine::Physic::PhysicEngine *enginePass) +{ + const btVector3 btstart(start.x, start.y, start.z); + const btVector3 btend(end.x, end.y, end.z); + + const btTransform &trans = actor->getWorldTransform(); + btTransform from(trans); + btTransform to(trans); + from.setOrigin(btstart); + to.setOrigin(btend); + + ClosestNotMeConvexResultCallback newTraceCallback(actor, btstart-btend, btScalar(0.0)); + newTraceCallback.m_collisionFilterMask = OEngine::Physic::CollisionType_World | + OEngine::Physic::CollisionType_HeightMap | + OEngine::Physic::CollisionType_Actor; + + btCollisionShape *shape = actor->getCollisionShape(); + assert(shape->isConvex()); + enginePass->dynamicsWorld->convexSweepTest(static_cast(shape), + from, to, newTraceCallback); + + // Copy the hit data over to our trace results struct: + if(newTraceCallback.hasHit()) + { + const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; + results->fraction = newTraceCallback.m_closestHitFraction; + results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); + results->endpos = (end-start)*results->fraction + start; + } + else + { + results->endpos = end; + results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); + results->fraction = 1.0f; + } +} diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index cd2547f8c..79c6e72ef 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -13,6 +13,9 @@ namespace OEngine } +class btCollisionObject; + + struct traceResults { Ogre::Vector3 endpos; @@ -22,5 +25,6 @@ struct traceResults }; void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine* enginePass); +void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); #endif From b35110964908240e954138603e7a14f5b7fdaf75 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 03:55:04 -0700 Subject: [PATCH 039/194] Get rid of the old newtrace method --- apps/openmw/mwworld/physicssystem.cpp | 31 ++++++++---------------- libs/openengine/bullet/trace.cpp | 35 +-------------------------- libs/openengine/bullet/trace.h | 1 - 3 files changed, 11 insertions(+), 56 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index ea013bc7a..65cc806ea 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -87,36 +87,25 @@ namespace MWWorld const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); - bool hit=false; - bool isInterior = !ptr.getCell()->isExterior(); - OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle()); if (!physicActor) return position; - bool wasCollisionMode = physicActor->getCollisionMode(); - physicActor->enableCollisions(false); - - Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); - halfExtents.z = 1; // we trace the feet only, so we use a very thin box + const int maxHeight = 64.f; + Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); - Ogre::Vector3 newPosition = position; + traceResults trace; + actortrace(&trace, physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); + if(trace.fraction >= 1.0f) + return position; - traceResults trace; //no initialization needed + physicActor->setOnGround(getSlope(trace.planenormal) <= sMaxSlope); - int maxHeight = 200.f; - newtrace(&trace, Ogre::Quaternion::IDENTITY, newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), halfExtents, isInterior, engine); - if(trace.fraction < 1.0f) - hit = true; newPosition = trace.endpos; + newPosition.z -= physicActor->getHalfExtents().z; + newPosition.z += 4.0f; - physicActor->setOnGround(hit && getSlope(trace.planenormal) <= sMaxSlope); - if (wasCollisionMode) - physicActor->enableCollisions(true); - - if(!hit) - return position; - return newPosition+Ogre::Vector3(0,0,4); + return newPosition; } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index 08c42d9d0..744462a0c 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -9,39 +9,6 @@ #include "physic.hpp" -void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine *enginePass) //Traceobj was a Aedra Object -{ - const btVector3 btstart(start.x, start.y, start.z + BBHalfExtents.z); - const btVector3 btend(end.x, end.y, end.z + BBHalfExtents.z); - const btQuaternion btorient (orient.x, orient.y, orient.z, orient.w); - - const btBoxShape newshape(btVector3(BBHalfExtents.x, BBHalfExtents.y, BBHalfExtents.z)); - //const btCapsuleShapeZ newshape(BBHalfExtents.x, BBHalfExtents.z * 2 - BBHalfExtents.x * 2); - const btTransform from(btorient, btstart); - const btTransform to(btorient, btend); - - btCollisionWorld::ClosestConvexResultCallback newTraceCallback(btstart, btend); - newTraceCallback.m_collisionFilterMask = OEngine::Physic::CollisionType_World|OEngine::Physic::CollisionType_HeightMap|OEngine::Physic::CollisionType_Actor; - - enginePass->dynamicsWorld->convexSweepTest(&newshape, from, to, newTraceCallback); - - // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) - { - const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; - results->fraction = newTraceCallback.m_closestHitFraction; - results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); - results->endpos = (end-start)*results->fraction + start; - } - else - { - results->endpos = end; - results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); - results->fraction = 1.0f; - } -} - - class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: @@ -62,7 +29,7 @@ public: else { ///need to transform normal into worldspace - hitNormalWorld = m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; } // NOTE : m_hitNormalLocal is not always vertical on the ground with a capsule or a box... diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index 79c6e72ef..6353d6cfa 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -24,7 +24,6 @@ struct traceResults float fraction; }; -void newtrace(traceResults *results, const Ogre::Quaternion& orient, const Ogre::Vector3& start, const Ogre::Vector3& end, const Ogre::Vector3& BBHalfExtents, bool isInterior, OEngine::Physic::PhysicEngine* enginePass); void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); #endif From 394fc7569778e092b52a8d1073cea31513b29b27 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 04:42:35 -0700 Subject: [PATCH 040/194] Clean up the trace struct --- apps/openmw/mwworld/physicssystem.cpp | 55 ++++++++++++++------------- libs/openengine/bullet/trace.cpp | 23 +++++++---- libs/openengine/bullet/trace.h | 30 +++++++-------- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 65cc806ea..ee6514590 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -45,21 +45,22 @@ namespace MWWorld const Ogre::Vector3 &velocity, float &remainingTime, OEngine::Physic::PhysicEngine *engine) { - traceResults trace; - actortrace(&trace, colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(trace.fraction == 0.0f) + OEngine::Physic::ActorTracer tracer; + tracer.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(tracer.mFraction == 0.0f) return false; - actortrace(&trace, colobj, trace.endpos, trace.endpos + velocity*remainingTime, engine); - if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > sMaxSlope)) + tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos + velocity*remainingTime, engine); + if(tracer.mFraction < std::numeric_limits::epsilon() || + (tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) > sMaxSlope)) return false; - float movefrac = trace.fraction; + float movefrac = tracer.mFraction; - actortrace(&trace, colobj, trace.endpos, trace.endpos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(getSlope(trace.planenormal) <= sMaxSlope) + tracer.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + if(getSlope(tracer.mPlaneNormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - position = trace.endpos; + position = tracer.mEndPos; remainingTime *= (1.0f-movefrac); return true; } @@ -94,16 +95,16 @@ namespace MWWorld const int maxHeight = 64.f; Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); - traceResults trace; - actortrace(&trace, physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); - if(trace.fraction >= 1.0f) + OEngine::Physic::ActorTracer tracer; + tracer.doTrace(physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); + if(tracer.mFraction >= 1.0f) return position; - physicActor->setOnGround(getSlope(trace.planenormal) <= sMaxSlope); + physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); - newPosition = trace.endpos; + newPosition = tracer.mEndPos; newPosition.z -= physicActor->getHalfExtents().z; - newPosition.z += 4.0f; + newPosition.z += 2.0f; return newPosition; } @@ -129,7 +130,7 @@ namespace MWWorld Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); position.z += halfExtents.z; - traceResults trace; + OEngine::Physic::ActorTracer tracer; bool onground = false; const Ogre::Quaternion orient; // Don't rotate actor collision boxes Ogre::Vector3 velocity; @@ -144,8 +145,8 @@ namespace MWWorld { if(!(movement.z > 0.0f)) { - actortrace(&trace, colobj, position, position-Ogre::Vector3(0,0,4), engine); - if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) + tracer.doTrace(colobj, position, position-Ogre::Vector3(0,0,4), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) onground = true; } velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)*movement / time; @@ -164,13 +165,13 @@ namespace MWWorld for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) { // trace to where character would go if there were no obstructions - actortrace(&trace, colobj, newPosition, newPosition+velocity*remainingTime, engine); + tracer.doTrace(colobj, newPosition, newPosition+velocity*remainingTime, engine); // check for obstructions - if(trace.fraction >= 1.0f) + if(tracer.mFraction >= 1.0f) { - newPosition = trace.endpos; - remainingTime *= (1.0f-trace.fraction); + newPosition = tracer.mEndPos; + remainingTime *= (1.0f-tracer.mFraction); break; } @@ -182,9 +183,9 @@ namespace MWWorld { // Can't move this way, try to find another spot along the plane Ogre::Real movelen = velocity.normalise(); - Ogre::Vector3 reflectdir = velocity.reflect(trace.planenormal); + Ogre::Vector3 reflectdir = velocity.reflect(tracer.mPlaneNormal); reflectdir.normalise(); - velocity = slide(reflectdir, trace.planenormal)*movelen; + velocity = slide(reflectdir, tracer.mPlaneNormal)*movelen; // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. @@ -195,9 +196,9 @@ namespace MWWorld if(onground) { - actortrace(&trace, colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+4.0f), engine); - if(trace.fraction < 1.0f && getSlope(trace.planenormal) <= sMaxSlope) - newPosition.z = trace.endpos.z + 2.0f; + tracer.doTrace(colobj, newPosition, newPosition-Ogre::Vector3(0,0,sStepSize+4.0f), engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) + newPosition.z = tracer.mEndPos.z + 2.0f; else onground = false; } diff --git a/libs/openengine/bullet/trace.cpp b/libs/openengine/bullet/trace.cpp index 744462a0c..5edf53620 100644 --- a/libs/openengine/bullet/trace.cpp +++ b/libs/openengine/bullet/trace.cpp @@ -9,6 +9,11 @@ #include "physic.hpp" +namespace OEngine +{ +namespace Physic +{ + class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: @@ -47,7 +52,8 @@ protected: const btScalar mMinSlopeDot; }; -void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, OEngine::Physic::PhysicEngine *enginePass) + +void ActorTracer::doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, const PhysicEngine *enginePass) { const btVector3 btstart(start.x, start.y, start.z); const btVector3 btend(end.x, end.y, end.z); @@ -72,14 +78,17 @@ void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vec if(newTraceCallback.hasHit()) { const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld; - results->fraction = newTraceCallback.m_closestHitFraction; - results->planenormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); - results->endpos = (end-start)*results->fraction + start; + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Ogre::Vector3(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z()); + mEndPos = (end-start)*mFraction + start; } else { - results->endpos = end; - results->planenormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); - results->fraction = 1.0f; + mEndPos = end; + mPlaneNormal = Ogre::Vector3(0.0f, 0.0f, 1.0f); + mFraction = 1.0f; } } + +} +} diff --git a/libs/openengine/bullet/trace.h b/libs/openengine/bullet/trace.h index 6353d6cfa..f81579c2e 100644 --- a/libs/openengine/bullet/trace.h +++ b/libs/openengine/bullet/trace.h @@ -4,26 +4,26 @@ #include -namespace OEngine -{ - namespace Physic - { - class PhysicEngine; - } -} - - class btCollisionObject; -struct traceResults +namespace OEngine +{ +namespace Physic { - Ogre::Vector3 endpos; - Ogre::Vector3 planenormal; + class PhysicEngine; + + struct ActorTracer + { + Ogre::Vector3 mEndPos; + Ogre::Vector3 mPlaneNormal; - float fraction; -}; + float mFraction; -void actortrace(traceResults *results, btCollisionObject *actor, const Ogre::Vector3& start, const Ogre::Vector3& end, OEngine::Physic::PhysicEngine *enginePass); + void doTrace(btCollisionObject *actor, const Ogre::Vector3 &start, const Ogre::Vector3 &end, + const PhysicEngine *enginePass); + }; +} +} #endif From d727b155805d23f07a95115896ea88b0715cd7e9 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 17 Aug 2013 04:55:35 -0700 Subject: [PATCH 041/194] Fix tracing down --- apps/openmw/mwworld/physicssystem.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index ee6514590..1beb5a7e5 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -92,9 +92,10 @@ namespace MWWorld if (!physicActor) return position; - const int maxHeight = 64.f; - Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, 4.0f); + const Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); + Ogre::Vector3 newPosition = position+Ogre::Vector3(0.0f, 0.0f, halfExtents.z); + const int maxHeight = 200.f; OEngine::Physic::ActorTracer tracer; tracer.doTrace(physicActor->getCollisionBody(), newPosition, newPosition-Ogre::Vector3(0,0,maxHeight), engine); if(tracer.mFraction >= 1.0f) @@ -103,7 +104,7 @@ namespace MWWorld physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); newPosition = tracer.mEndPos; - newPosition.z -= physicActor->getHalfExtents().z; + newPosition.z -= halfExtents.z; newPosition.z += 2.0f; return newPosition; From ebf9debb80b510113dcef60a4d5c3d6dd2febc15 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 19 Aug 2013 20:30:22 +0200 Subject: [PATCH 042/194] Enabled terrain self shadows, implemented getHeightAt, some optimizations --- apps/openmw/engine.cpp | 8 +- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwgui/settingswindow.cpp | 10 +- apps/openmw/mwgui/settingswindow.hpp | 2 +- apps/openmw/mwrender/renderingmanager.cpp | 35 ++-- apps/openmw/mwrender/renderingmanager.hpp | 6 +- apps/openmw/mwrender/shadows.cpp | 3 +- apps/openmw/mwworld/scene.cpp | 6 +- apps/openmw/mwworld/worldimp.cpp | 26 ++- apps/openmw/mwworld/worldimp.hpp | 2 +- components/terrain/chunk.cpp | 8 +- components/terrain/material.cpp | 7 +- components/terrain/material.hpp | 1 + components/terrain/quadtreenode.cpp | 141 +++++++++++----- components/terrain/quadtreenode.hpp | 24 ++- components/terrain/storage.cpp | 190 +++++++++++++++++++--- components/terrain/storage.hpp | 6 + components/terrain/terrain.cpp | 65 +++++--- components/terrain/terrain.hpp | 22 ++- files/materials/terrain.shader | 1 + files/mygui/openmw_settings_window.layout | 8 +- files/settings-default.cfg | 6 +- 22 files changed, 418 insertions(+), 161 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 23e94cb51..62a15fbf9 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -69,8 +69,8 @@ void OMW::Engine::setAnimationVerbose(bool animverbose) bool OMW::Engine::frameStarted (const Ogre::FrameEvent& evt) { - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) - MWBase::Environment::get().getWorld()->frameStarted(evt.timeSinceLastFrame); + bool paused = MWBase::Environment::get().getWindowManager()->isGuiMode(); + MWBase::Environment::get().getWorld()->frameStarted(evt.timeSinceLastFrame, paused); MWBase::Environment::get().getWindowManager ()->frameStarted(evt.timeSinceLastFrame); return true; } @@ -91,12 +91,12 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::Environment::get().getSoundManager()->update(frametime); // global scripts - //MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); bool changed = MWBase::Environment::get().getWorld()->hasCellChanged(); // local scripts - //executeLocalScripts(); // This does not handle the case where a global script causes a cell + executeLocalScripts(); // This does not handle the case where a global script causes a cell // change, followed by a cell change in a local script during the same // frame. diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 4b4ffd0d3..e2b8a236b 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -371,7 +371,7 @@ namespace MWBase /// \todo this does not belong here virtual void playVideo(const std::string& name, bool allowSkipping) = 0; virtual void stopVideo() = 0; - virtual void frameStarted (float dt) = 0; + virtual void frameStarted (float dt, bool paused) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 4ce6ac0bf..3dfa17bad 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -125,7 +125,7 @@ namespace MWGui getWidget(mActorShadows, "ActorShadows"); getWidget(mStaticsShadows, "StaticsShadows"); getWidget(mMiscShadows, "MiscShadows"); - getWidget(mShadowsDebug, "ShadowsDebug"); + getWidget(mTerrainShadows, "TerrainShadows"); getWidget(mControlsBox, "ControlsBox"); getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mInvertYButton, "InvertYButton"); @@ -161,7 +161,7 @@ namespace MWGui mActorShadows->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); mStaticsShadows->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); mMiscShadows->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); - mShadowsDebug->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); + mTerrainShadows->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); mMasterVolumeSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); mVoiceVolumeSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); @@ -238,7 +238,7 @@ namespace MWGui mActorShadows->setCaptionWithReplacing(Settings::Manager::getBool("actor shadows", "Shadows") ? "#{sOn}" : "#{sOff}"); mStaticsShadows->setCaptionWithReplacing(Settings::Manager::getBool("statics shadows", "Shadows") ? "#{sOn}" : "#{sOff}"); mMiscShadows->setCaptionWithReplacing(Settings::Manager::getBool("misc shadows", "Shadows") ? "#{sOn}" : "#{sOff}"); - mShadowsDebug->setCaptionWithReplacing(Settings::Manager::getBool("debug", "Shadows") ? "#{sOn}" : "#{sOff}"); + mTerrainShadows->setCaptionWithReplacing(Settings::Manager::getBool("terrain shadows", "Shadows") ? "#{sOn}" : "#{sOff}"); float cameraSens = (Settings::Manager::getFloat("camera sensitivity", "Input")-0.2)/(5.0-0.2); mCameraSensitivitySlider->setScrollPosition (cameraSens * (mCameraSensitivitySlider->getScrollRange()-1)); @@ -394,8 +394,8 @@ namespace MWGui Settings::Manager::setBool("statics shadows", "Shadows", newState); else if (_sender == mMiscShadows) Settings::Manager::setBool("misc shadows", "Shadows", newState); - else if (_sender == mShadowsDebug) - Settings::Manager::setBool("debug", "Shadows", newState); + else if (_sender == mTerrainShadows) + Settings::Manager::setBool("terrain shadows", "Shadows", newState); else if (_sender == mInvertYButton) Settings::Manager::setBool("invert y axis", "Input", newState); else if (_sender == mCrosshairButton) diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 42ed5bf6d..a585bda7e 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -59,7 +59,7 @@ namespace MWGui MyGUI::Button* mActorShadows; MyGUI::Button* mStaticsShadows; MyGUI::Button* mMiscShadows; - MyGUI::Button* mShadowsDebug; + MyGUI::Button* mTerrainShadows; // audio MyGUI::ScrollBar* mMasterVolumeSlider; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 9f7fde10a..63de044c8 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -82,7 +82,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b Settings::Manager::setString("shader mode", "General", openGL ? (glES ? "glsles" : "glsl") : "hlsl"); } - mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 50); + mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 5); mRendering.getWindow()->addListener(this); mRendering.setWindowListener(this); @@ -248,8 +248,10 @@ void RenderingManager::cellAdded (MWWorld::Ptr::CellStore *store) { if (!mTerrain) { - mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain); - mTerrain->update(mRendering.getCamera()); + mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + Settings::Manager::getBool("distant land", "Terrain"), + Settings::Manager::getBool("shader", "Terrain")); + mTerrain->update(mRendering.getCamera()->getRealPosition()); } } waterAdded(store); @@ -554,13 +556,6 @@ void RenderingManager::setAmbientMode() } } -float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) -{ - assert(mTerrain); - return mTerrain->getHeightAt(worldPos); -} - - void RenderingManager::configureAmbient(MWWorld::Ptr::CellStore &mCell) { mAmbientColor.setAsABGR (mCell.mCell->mAmbi.mAmbient); @@ -658,9 +653,14 @@ void RenderingManager::requestMap(MWWorld::Ptr::CellStore* cell) { if (cell->mCell->isExterior()) { + assert(mTerrain); + Ogre::AxisAlignedBox dims = mObjects.getDimensions(cell); - Ogre::Vector2 center(cell->mCell->getGridX() + 0.5, -cell->mCell->getGridY() + 1 - 0.5); + Ogre::Vector2 center(cell->mCell->getGridX() + 0.5, cell->mCell->getGridY() + 0.5); dims.merge(mTerrain->getWorldBoundingBox(center)); + + mTerrain->update(dims.getCenter()); + mLocalMap->requestMap(cell, dims.getMinimum().z, dims.getMaximum().z); } else @@ -992,12 +992,13 @@ void RenderingManager::updateWaterRippleEmitterPtr (const MWWorld::Ptr& old, con mWater->updateEmitterPtr(old, ptr); } -void RenderingManager::frameStarted(float dt) +void RenderingManager::frameStarted(float dt, bool paused) { if (mTerrain) - mTerrain->update(mRendering.getCamera()); + mTerrain->update(mRendering.getCamera()->getRealPosition()); - mWater->frameStarted(dt); + if (!paused) + mWater->frameStarted(dt); } void RenderingManager::resetCamera() @@ -1005,4 +1006,10 @@ void RenderingManager::resetCamera() mCamera->reset(); } +float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) +{ + assert(mTerrain); + return mTerrain->getHeightAt(worldPos); +} + } // namespace diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index c4fb05804..ea6e8d409 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -157,6 +157,8 @@ public: bool occlusionQuerySupported() { return mOcclusionQuery->supported(); } OcclusionQuery* getOcclusionQuery() { return mOcclusionQuery; } + float getTerrainHeightAt (Ogre::Vector3 worldPos); + Shadows* getShadows(); void switchToInterior(); @@ -164,8 +166,6 @@ public: void getTriangleBatchCount(unsigned int &triangles, unsigned int &batches); - float getTerrainHeightAt (Ogre::Vector3 worldPos); - void setGlare(bool glare); void skyEnable (); void skyDisable (); @@ -209,7 +209,7 @@ public: void playVideo(const std::string& name, bool allowSkipping); void stopVideo(); - void frameStarted(float dt); + void frameStarted(float dt, bool paused); protected: virtual void windowResized(int x, int y); diff --git a/apps/openmw/mwrender/shadows.cpp b/apps/openmw/mwrender/shadows.cpp index c28c01dcc..21bbe51b6 100644 --- a/apps/openmw/mwrender/shadows.cpp +++ b/apps/openmw/mwrender/shadows.cpp @@ -107,7 +107,8 @@ void Shadows::recreate() // Set visibility mask for the shadow render textures int visibilityMask = RV_Actors * Settings::Manager::getBool("actor shadows", "Shadows") + (RV_Statics + RV_StaticsSmall) * Settings::Manager::getBool("statics shadows", "Shadows") - + RV_Misc * Settings::Manager::getBool("misc shadows", "Shadows"); + + RV_Misc * Settings::Manager::getBool("misc shadows", "Shadows") + + RV_Terrain * (Settings::Manager::getBool("terrain shadows", "Shadows")); for (int i = 0; i < (split ? 3 : 1); ++i) { TexturePtr shadowTexture = mSceneMgr->getShadowTexture(i); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 03ddf2aa9..07155d349 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -1,5 +1,7 @@ #include "scene.hpp" +#include + #include #include @@ -85,7 +87,6 @@ namespace MWWorld std::cout << "Unloading cell\n"; ListAndResetHandles functor; - /* (*iter)->forEach(functor); { // silence annoying g++ warning @@ -96,7 +97,6 @@ namespace MWWorld mPhysics->removeObject (node->getName()); } } - */ if ((*iter)->mCell->isExterior()) { @@ -150,7 +150,7 @@ namespace MWWorld // ... then references. This is important for adjustPosition to work correctly. /// \todo rescale depending on the state of a new GMST - //insertCell (*cell, true); + insertCell (*cell, true); mRendering.cellAdded (cell); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ced4b5faa..f0c55c43e 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -835,7 +835,7 @@ namespace MWWorld bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || mWorldScene->isCellActive(*currCell); - if (false ) //*currCell != newCell) + if (*currCell != newCell) { removeContainerScripts(ptr); @@ -1026,10 +1026,13 @@ namespace MWWorld return; } - float terrainHeight = -std::numeric_limits().max();// mRendering->getTerrainHeightAt(pos); + if (ptr.getCell()->isExterior()) + { + float terrainHeight = mRendering->getTerrainHeightAt(pos); - if (pos.z < terrainHeight) - pos.z = terrainHeight; + if (pos.z < terrainHeight) + pos.z = terrainHeight; + } ptr.getRefData().getPosition().pos[2] = pos.z + 20; // place slightly above. will snap down to ground with code below @@ -1074,15 +1077,8 @@ namespace MWWorld { const int cellSize = 8192; - cellX = static_cast (x/cellSize); - - if (x<0) - --cellX; - - cellY = static_cast (y/cellSize); - - if (y<0) - --cellY; + cellX = std::floor(x/cellSize); + cellY = std::floor(y/cellSize); } void World::doPhysics(const PtrMovementList &actors, float duration) @@ -1682,9 +1678,9 @@ namespace MWWorld mRendering->stopVideo(); } - void World::frameStarted (float dt) + void World::frameStarted (float dt, bool paused) { - mRendering->frameStarted(dt); + mRendering->frameStarted(dt, paused); } void World::activateDoor(const MWWorld::Ptr& door) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b2f6418a3..1e072e09a 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -415,7 +415,7 @@ namespace MWWorld /// \todo this does not belong here virtual void playVideo(const std::string& name, bool allowSkipping); virtual void stopVideo(); - virtual void frameStarted (float dt); + virtual void frameStarted (float dt, bool paused); /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp index c9a364dc4..7a81358d6 100644 --- a/components/terrain/chunk.cpp +++ b/components/terrain/chunk.cpp @@ -2,9 +2,6 @@ #include #include -#include -#include -#include #include "quadtreenode.hpp" #include "terrain.hpp" @@ -53,7 +50,6 @@ namespace Terrain mColourBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR), mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); - mNode->getTerrain()->getStorage()->fillVertexBuffers(lodLevel, mNode->getSize(), mNode->getCenter(), mVertexBuffer, mNormalBuffer, mColourBuffer); @@ -66,6 +62,8 @@ namespace Terrain mIndexData->indexStart = 0; } + + void Chunk::updateIndexBuffer() { // Fetch a suitable index buffer (which may be shared) @@ -75,7 +73,7 @@ namespace Terrain for (int i=0; i<4; ++i) { - QuadTreeNode* neighbour = mNode->searchNeighbour((Direction)i); + QuadTreeNode* neighbour = mNode->getNeighbour((Direction)i); // If the neighbour isn't currently rendering itself, // go up until we find one. NOTE: We don't need to go down, diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index bd7501576..f85326492 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -106,6 +106,8 @@ namespace Terrain Ogre::Pass* pass = technique->createPass(); pass->setLightingEnabled(false); pass->setVertexColourTracking(Ogre::TVC_NONE); + // TODO: How to handle fog? + pass->setFog(true, Ogre::FOG_NONE); bool first = (layer == mLayerList.begin()); @@ -127,9 +129,7 @@ namespace Terrain tus->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); float scale = (16/(16.f+1.f)); - float scroll = 1/16.f*0.5; - tus->setTextureScale(scale,scale); - tus->setTextureScroll(-scroll,-scroll); + tus->setTextureScale(1.f/scale,1.f/scale); } // Add the actual layer texture on top of the alpha map. @@ -150,6 +150,7 @@ namespace Terrain Ogre::Pass* lightingPass = technique->createPass(); lightingPass->setSceneBlending(Ogre::SBT_MODULATE); lightingPass->setVertexColourTracking(Ogre::TVC_AMBIENT|Ogre::TVC_DIFFUSE); + lightingPass->setFog(true, Ogre::FOG_NONE); } } diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index 749e806ad..068bb7a84 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -18,6 +18,7 @@ namespace Terrain void setLayerList (const std::vector& layerList) { mLayerList = layerList; } bool hasLayers() { return mLayerList.size(); } void setBlendmapList (const std::vector& blendmapList) { mBlendmapList = blendmapList; } + const std::vector& getBlendmapList() { return mBlendmapList; } void setCompositeMap (const std::string& name) { mCompositeMap = name; } /// Creates a material suitable for displaying a chunk of terrain using alpha-blending. diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 8ebfafeb4..ce94d62a7 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -142,17 +142,21 @@ QuadTreeNode::QuadTreeNode(Terrain* terrain, ChildDirection dir, float size, con , mTerrain(terrain) , mChunk(NULL) , mMaterialGenerator(NULL) + , mBounds(Ogre::AxisAlignedBox::BOX_NULL) + , mWorldBounds(Ogre::AxisAlignedBox::BOX_NULL) { mBounds.setNull(); for (int i=0; i<4; ++i) mChildren[i] = NULL; + for (int i=0; i<4; ++i) + mNeighbours[i] = NULL; mSceneNode = mTerrain->getSceneManager()->getRootSceneNode()->createChildSceneNode( Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); mLodLevel = log2(mSize); - mMaterialGenerator = new MaterialGenerator(true); + mMaterialGenerator = new MaterialGenerator(mTerrain->getShadersEnabled()); } void QuadTreeNode::createChild(ChildDirection id, float size, const Ogre::Vector2 ¢er) @@ -168,38 +172,40 @@ QuadTreeNode::~QuadTreeNode() delete mMaterialGenerator; } -QuadTreeNode* QuadTreeNode::searchNeighbour(Direction dir) +QuadTreeNode* QuadTreeNode::getNeighbour(Direction dir) { - return searchNeighbourRecursive(this, dir); + return mNeighbours[static_cast(dir)]; } -const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox() +void QuadTreeNode::initNeighbours() { - if (mIsDummy) - return Ogre::AxisAlignedBox::BOX_NULL; - if (mBounds.isNull()) + for (int i=0; i<4; ++i) + mNeighbours[i] = searchNeighbourRecursive(this, (Direction)i); +} + +void QuadTreeNode::initAabb() +{ + if (hasChildren()) { - if (hasChildren()) + for (int i=0; i<4; ++i) { - // X and Y are obvious, just need Z - float min = std::numeric_limits().max(); - float max = -std::numeric_limits().max(); - for (int i=0; i<4; ++i) - { - QuadTreeNode* child = getChild((ChildDirection)i); - float v = child->getBoundingBox().getMaximum().z; - if (v > max) - max = v; - v = child->getBoundingBox().getMinimum().z; - if (v < min) - min = v; - } - mBounds = Ogre::AxisAlignedBox (Ogre::Vector3(-mSize/2*8192, -mSize/2*8192, min), - Ogre::Vector3(mSize/2*8192, mSize/2*8192, max)); + mChildren[i]->initAabb(); + mBounds.merge(mChildren[i]->getBoundingBox()); } - else - throw std::runtime_error("Leaf node should have bounds set!"); + mBounds = Ogre::AxisAlignedBox (Ogre::Vector3(-mSize/2*8192, -mSize/2*8192, mBounds.getMinimum().z), + Ogre::Vector3(mSize/2*8192, mSize/2*8192, mBounds.getMaximum().z)); } + mWorldBounds = Ogre::AxisAlignedBox(mBounds.getMinimum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0), + mBounds.getMaximum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); +} + +void QuadTreeNode::setBoundingBox(const Ogre::AxisAlignedBox &box) +{ + mBounds = box; +} + +const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox() +{ return mBounds; } @@ -209,11 +215,19 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (bounds.isNull()) return; - Ogre::AxisAlignedBox worldBounds (bounds.getMinimum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0), - bounds.getMaximum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + float dist = distance(mWorldBounds, cameraPos); + + if (!mTerrain->getDistantLandEnabled()) + { + if (dist > 8192*2) + { + destroyChunks(); + return; + } + } - float dist = distance(worldBounds, cameraPos); /// \todo implement error metrics or some other means of not using arbitrary values + /// (general quality needs to be user configurable as well) size_t wantedLod = 0; if (dist > 8192*1) wantedLod = 1; @@ -230,6 +244,7 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (mSize <= mTerrain->getMaxBatchSize() && mLodLevel <= wantedLod) { + bool hadChunk = hasChunk(); // Wanted LOD is small enough to render this node in one chunk if (!mChunk) { @@ -250,14 +265,32 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) } } + // Additional (index buffer) LOD is currently disabled. + // This is due to a problem with the LOD selection when a node splits. + // After splitting, the distance is measured from the children's bounding boxes, which are possibly + // further away than the original node's bounding box, possibly causing a child to switch to a *lower* LOD + // than the original node. + // In short, we'd sometimes get a switch to a lesser detail when actually moving closer. + // This wouldn't be so bad, but unfortunately it also breaks LOD edge connections if a neighbour + // node hasn't split yet, and has a higher LOD than our node's child: + // ----- ----- ------------ + // | LOD | LOD | | + // | 1 | 1 | | + // |-----|-----| LOD 0 | + // | LOD | LOD | | + // | 0 | 0 | | + // ----- ----- ------------ + // To prevent this, nodes of the same size need to always select the same LOD, which is basically what we're + // doing here. + // But this "solution" does increase triangle overhead, so eventually we need to find a more clever way. + //mChunk->setAdditionalLod(wantedLod - mLodLevel); - mChunk->setAdditionalLod(wantedLod - mLodLevel); mChunk->setVisible(true); - if (hasChildren()) + if (!hadChunk && hasChildren()) { for (int i=0; i<4; ++i) - mChildren[i]->removeChunks(); + mChildren[i]->hideChunks(); } } else @@ -272,15 +305,41 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) } } -void QuadTreeNode::removeChunks() +void QuadTreeNode::hideChunks() { if (mChunk) mChunk->setVisible(false); - if (hasChildren()) - { + else if (hasChildren()) for (int i=0; i<4; ++i) - mChildren[i]->removeChunks(); + mChildren[i]->hideChunks(); +} + +void QuadTreeNode::destroyChunks() +{ + if (mChunk) + { + Ogre::MaterialManager::getSingleton().remove(mChunk->getMaterial()->getName()); + mSceneNode->detachObject(mChunk); + + delete mChunk; + mChunk = NULL; + // destroy blendmaps + if (mMaterialGenerator) + { + const std::vector& list = mMaterialGenerator->getBlendmapList(); + for (std::vector::const_iterator it = list.begin(); it != list.end(); ++it) + Ogre::TextureManager::getSingleton().remove((*it)->getName()); + mMaterialGenerator->setBlendmapList(std::vector()); + mMaterialGenerator->setLayerList(std::vector()); + mMaterialGenerator->setCompositeMap(""); + } + + Ogre::TextureManager::getSingleton().remove(mCompositeMap->getName()); + mCompositeMap.setNull(); } + else if (hasChildren()) + for (int i=0; i<4; ++i) + mChildren[i]->destroyChunks(); } void QuadTreeNode::updateIndexBuffers() @@ -312,7 +371,7 @@ void QuadTreeNode::ensureLayerInfo() std::vector blendmaps; std::vector layerList; - mTerrain->getStorage()->getBlendmaps(mSize, mCenter, true, blendmaps, layerList); + mTerrain->getStorage()->getBlendmaps(mSize, mCenter, mTerrain->getShadersEnabled(), blendmaps, layerList); mMaterialGenerator->setLayerList(layerList); mMaterialGenerator->setBlendmapList(blendmaps); @@ -324,7 +383,9 @@ void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) if (mIsDummy) { - MaterialGenerator matGen(true); + // TODO - why is this completely black? + // TODO - store this default material somewhere instead of creating one for each empty cell + MaterialGenerator matGen(mTerrain->getShadersEnabled()); std::vector layer; layer.push_back("_land_default.dds"); matGen.setLayerList(layer); @@ -357,12 +418,6 @@ void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) } } - -bool QuadTreeNode::hasCompositeMap() -{ - return !mCompositeMap.isNull(); -} - void QuadTreeNode::ensureCompositeMap() { if (!mCompositeMap.isNull()) diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index b68d6ab82..1b7c71b5b 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -49,6 +49,13 @@ namespace Terrain QuadTreeNode (Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); ~QuadTreeNode(); + /// Initialize neighbours - do this after the quadtree is built + void initNeighbours(); + /// Initialize bounding boxes of non-leafs by merging children bounding boxes. + /// Do this after the quadtree is built - note that leaf bounding boxes + /// need to be set first via setBoundingBox! + void initAabb(); + /// @note takes ownership of \a child void createChild (ChildDirection id, float size, const Ogre::Vector2& center); @@ -66,15 +73,15 @@ namespace Terrain bool hasChildren() { return mChildren[0] != 0; } QuadTreeNode* getChild(ChildDirection dir) { return mChildren[dir]; } - /// Search for a neighbour node in this direction - QuadTreeNode* searchNeighbour (Direction dir); + /// Get neighbour node in this direction + QuadTreeNode* getNeighbour (Direction dir); /// Returns our direction relative to the parent node, or Root if we are the root node. ChildDirection getDirection() { return mDirection; } /// Set bounding box in local coordinates. Should be done at load time for leaf nodes. /// Other nodes can merge AABB of child nodes. - void setBoundingBox (const Ogre::AxisAlignedBox& box) { mBounds = box; } + void setBoundingBox (const Ogre::AxisAlignedBox& box); /// Get bounding box in local coordinates const Ogre::AxisAlignedBox& getBoundingBox(); @@ -88,8 +95,11 @@ namespace Terrain /// Call after QuadTreeNode::update! void updateIndexBuffers(); - /// Remove chunks rendered by this node and all its children - void removeChunks(); + /// Hide chunks rendered by this node and all its children + void hideChunks(); + + /// Destroy chunks rendered by this node and all its children + void destroyChunks(); /// Get the effective LOD level if this node was rendered in one chunk /// with ESM::Land::LAND_SIZE^2 vertices @@ -101,8 +111,6 @@ namespace Terrain /// Is this node currently configured to render itself? bool hasChunk(); - bool hasCompositeMap(); - /// Add a textured quad to a specific 2d area in the composite map scenemanager. /// Only nodes with size <= 1 can be rendered with alpha blending, so larger nodes will simply /// call this method on their children. @@ -118,6 +126,7 @@ namespace Terrain float mSize; size_t mLodLevel; // LOD if we were to render this node in one chunk Ogre::AxisAlignedBox mBounds; + Ogre::AxisAlignedBox mWorldBounds; ChildDirection mDirection; Ogre::Vector2 mCenter; @@ -125,6 +134,7 @@ namespace Terrain QuadTreeNode* mParent; QuadTreeNode* mChildren[4]; + QuadTreeNode* mNeighbours[4]; Chunk* mChunk; diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp index eb2763d94..800af3282 100644 --- a/components/terrain/storage.cpp +++ b/components/terrain/storage.cpp @@ -54,15 +54,25 @@ namespace Terrain void Storage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) { - if (col == ESM::Land::LAND_SIZE-1) + while (col >= ESM::Land::LAND_SIZE-1) { ++cellY; - col = 0; + col -= ESM::Land::LAND_SIZE-1; } - if (row == ESM::Land::LAND_SIZE-1) + while (row >= ESM::Land::LAND_SIZE-1) { ++cellX; - row = 0; + row -= ESM::Land::LAND_SIZE-1; + } + while (col < 0) + { + --cellY; + col += ESM::Land::LAND_SIZE-1; + } + while (row < 0) + { + --cellX; + row += ESM::Land::LAND_SIZE-1; } ESM::Land* land = getLand(cellX, cellY); if (land && land->mHasData) @@ -70,7 +80,49 @@ namespace Terrain normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + normal.normalise(); } + else + normal = Ogre::Vector3(0,0,1); + } + + void Storage::averageNormal(Ogre::Vector3 &normal, int cellX, int cellY, int col, int row) + { + Ogre::Vector3 n1,n2,n3,n4; + fixNormal(n1, cellX, cellY, col+1, row); + fixNormal(n2, cellX, cellY, col-1, row); + fixNormal(n3, cellX, cellY, col, row+1); + fixNormal(n4, cellX, cellY, col, row-1); + normal = (n1+n2+n3+n4); + normal.normalise(); + } + + void Storage::fixColour (Ogre::ColourValue& color, int cellX, int cellY, int col, int row) + { + if (col == ESM::Land::LAND_SIZE-1) + { + ++cellY; + col = 0; + } + if (row == ESM::Land::LAND_SIZE-1) + { + ++cellX; + row = 0; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mLandData->mUsingColours) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + } void Storage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, @@ -141,17 +193,21 @@ namespace Terrain normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; - // Normals don't connect seamlessly between cells - wtf? - if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) - fixNormal(normal, cellX, cellY, col, row); - // z < 0 should never happen, but it does - I hate this data set... - if (normal.z < 0) - normal *= -1; normal.normalise(); } else normal = Ogre::Vector3(0,0,1); + // Normals apparently don't connect seamlessly between cells + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixNormal(normal, cellX, cellY, col, row); + + // some corner normals appear to be complete garbage (z < 0) + if ((row == 0 || row == ESM::Land::LAND_SIZE-1) && (col == 0 || col == ESM::Land::LAND_SIZE-1)) + averageNormal(normal, cellX, cellY, col, row); + + assert(normal.z > 0); + normals[vertX*numVerts*3 + vertY*3] = normal.x; normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; @@ -168,6 +224,11 @@ namespace Terrain color.g = 1; color.b = 1; } + + // Unlike normals, colors mostly connect seamlessly between cells, but not always... + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixColour(color, cellX, cellY, col, row); + color.a = 1; Ogre::uint32 rsColor; Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); @@ -193,18 +254,20 @@ namespace Terrain Storage::UniqueTextureId Storage::getVtexIndexAt(int cellX, int cellY, int x, int y) { - // If we're at the last row (or last column), we need to get the texture from the neighbour cell - // to get consistent blending at the border - if (x >= ESM::Land::LAND_TEXTURE_SIZE) + // For the first/last row/column, we need to get the texture from the neighbour cell + // to get consistent blending at the borders + --x; + if (x < 0) { - cellX++; - x -= ESM::Land::LAND_TEXTURE_SIZE; + --cellX; + x += ESM::Land::LAND_TEXTURE_SIZE; } - if (y >= ESM::Land::LAND_TEXTURE_SIZE) + if (y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? { - cellY++; + ++cellY; y -= ESM::Land::LAND_TEXTURE_SIZE; } + assert(x &blendmaps, std::vector &layerList) { + // TODO - blending isn't completely right yet; the blending radius appears to be + // different at a cell transition (2 vertices, not 4), so we may need to create a larger blendmap + // and interpolate the rest of the cell by hand? :/ + Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); int cellX = origin.x; int cellY = origin.y; @@ -253,7 +320,7 @@ namespace Terrain // To get a consistent look, we need to make sure to use the same base layer in all cells. // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. textureIndices.insert(std::make_pair(0,0)); - // NB +1 to get the last index from neighbour cell (see getVtexIndexAt) + for (int y=0; y(nX * factor); + int startY = static_cast(nY * factor); + int endX = startX + 1; + int endY = startY + 1; + + assert(endX < ESM::Land::LAND_SIZE); + assert(endY < ESM::Land::LAND_SIZE); + + // now get points in terrain space (effectively rounding them to boundaries) + float startXTS = startX * invFactor; + float startYTS = startY * invFactor; + float endXTS = endX * invFactor; + float endYTS = endY * invFactor; + + // get parametric from start coord to next point + float xParam = (nX - startXTS) * factor; + float yParam = (nY - startYTS) * factor; + + /* For even / odd tri strip rows, triangles are this shape: + even odd + 3---2 3---2 + | / | | \ | + 0---1 0---1 + */ + + // Build all 4 positions in terrain space, using point-sampled height + Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); + Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); + Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); + Ogre::Vector3 v3 (startXTS, endYTS, getVertexHeight(land, startX, endY) / 8192.f); + // define this plane in terrain space + Ogre::Plane plane; + // (At the moment, all rows have the same triangle alignment) + if (true) + { + // odd row + bool secondTri = ((1.0 - yParam) > xParam); + if (secondTri) + plane.redefine(v0, v1, v3); + else + plane.redefine(v1, v2, v3); + } + else + { + // even row + bool secondTri = (yParam > xParam); + if (secondTri) + plane.redefine(v0, v2, v3); + else + plane.redefine(v0, v1, v2); + } + + // Solve plane equation for z + return (-plane.normal.x * nX + -plane.normal.y * nY + - plane.d) / plane.normal.z * 8192; + + } + + float Storage::getVertexHeight(const ESM::Land *land, int x, int y) + { + assert(x < ESM::Land::LAND_SIZE); + assert(y < ESM::Land::LAND_SIZE); + return land->mLandData->mHeights[y * ESM::Land::LAND_SIZE + x]; + } + } diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index d55403d9c..419439e19 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -58,8 +58,14 @@ namespace Terrain std::vector& blendmaps, std::vector& layerList); + float getHeightAt (const Ogre::Vector3& worldPos); + private: void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + void fixColour (Ogre::ColourValue& colour, int cellX, int cellY, int col, int row); + void averageNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + + float getVertexHeight (const ESM::Land* land, int x, int y); // Since plugins can define new texture palettes, we need to know the plugin index too // in order to retrieve the correct texture name. diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp index c0f6cf2eb..d06394aca 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/terrain.cpp @@ -51,12 +51,14 @@ namespace namespace Terrain { - Terrain::Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags) + Terrain::Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags, bool distantLand, bool shaders) : mStorage(storage) , mMinBatchSize(1) , mMaxBatchSize(64) , mSceneMgr(sceneMgr) , mVisibilityFlags(visibilityFlags) + , mDistantLand(distantLand) + , mShaders(shaders) { mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); @@ -81,6 +83,8 @@ namespace Terrain mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(center.x, center.y), NULL); buildQuadTree(mRootNode); + mRootNode->initAabb(); + mRootNode->initNeighbours(); } Terrain::~Terrain() @@ -136,14 +140,19 @@ namespace Terrain node->markAsDummy(); } - void Terrain::update(Ogre::Camera *camera) + void Terrain::update(const Ogre::Vector3& cameraPos) { - mRootNode->update(camera->getRealPosition()); + mRootNode->update(cameraPos); mRootNode->updateIndexBuffers(); } Ogre::AxisAlignedBox Terrain::getWorldBoundingBox (const Ogre::Vector2& center) { + if (center.x > mBounds.getMaximum().x + || center.x < mBounds.getMinimum().x + || center.y > mBounds.getMaximum().y + || center.y < mBounds.getMinimum().y) + return Ogre::AxisAlignedBox::BOX_NULL; QuadTreeNode* node = findNode(center, mRootNode); Ogre::AxisAlignedBox box = node->getBoundingBox(); box.setExtents(box.getMinimum() + Ogre::Vector3(center.x, center.y, 0) * 8192, @@ -220,10 +229,10 @@ namespace Terrain for (size_t col = colStart; col < colEnd; col += increment) { indices.push_back(ESM::Land::LAND_SIZE*col+row); - indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row+increment); indices.push_back(ESM::Land::LAND_SIZE*col+row+increment); - indices.push_back(ESM::Land::LAND_SIZE*col+row+increment); + indices.push_back(ESM::Land::LAND_SIZE*col+row); indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row); indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row+increment); } @@ -242,17 +251,18 @@ namespace Terrain { indices.push_back(ESM::Land::LAND_SIZE*col+row); indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); - // Make sure not to touch the left edge - if (col == 0) - indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep); + // Make sure not to touch the right edge + if (col+outerStep == ESM::Land::LAND_SIZE-1) + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep-innerStep)+row+innerStep); else - indices.push_back(ESM::Land::LAND_SIZE*col+row+innerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row+innerStep); + for (size_t i = 0; i < outerStep; i += innerStep) { // Make sure not to touch the left or right edges if (col+i == 0 || col+i == ESM::Land::LAND_SIZE-1-innerStep) continue; - indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); + indices.push_back(ESM::Land::LAND_SIZE*(col)+row); indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row+innerStep); indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row+innerStep); } @@ -265,11 +275,11 @@ namespace Terrain { indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); indices.push_back(ESM::Land::LAND_SIZE*col+row); - // Make sure not to touch the right edge - if (col+outerStep == ESM::Land::LAND_SIZE-1) - indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep-innerStep)+row-innerStep); + // Make sure not to touch the left edge + if (col == 0) + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row-innerStep); else - indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row-innerStep); + indices.push_back(ESM::Land::LAND_SIZE*col+row-innerStep); for (size_t i = 0; i < outerStep; i += innerStep) { @@ -278,7 +288,7 @@ namespace Terrain continue; indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row-innerStep); indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row-innerStep); - indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); } } @@ -289,18 +299,18 @@ namespace Terrain { indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); indices.push_back(ESM::Land::LAND_SIZE*col+row); - // Make sure not to touch the bottom edge - if (row == 0) - indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep); + // Make sure not to touch the top edge + if (row+outerStep == ESM::Land::LAND_SIZE-1) + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+outerStep-innerStep); else - indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row); + indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+outerStep); for (size_t i = 0; i < outerStep; i += innerStep) { // Make sure not to touch the top or bottom edges if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep) continue; - indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); + indices.push_back(ESM::Land::LAND_SIZE*col+row); indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i); indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i+innerStep); } @@ -313,18 +323,18 @@ namespace Terrain { indices.push_back(ESM::Land::LAND_SIZE*col+row); indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); - // Make sure not to touch the top edge - if (row+outerStep == ESM::Land::LAND_SIZE-1) - indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep-innerStep); + // Make sure not to touch the bottom edge + if (row == 0) + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+innerStep); else - indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep); + indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row); for (size_t i = 0; i < outerStep; i += innerStep) { // Make sure not to touch the top or bottom edges if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep) continue; - indices.push_back(ESM::Land::LAND_SIZE*col+row); + indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i+innerStep); indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i); } @@ -355,5 +365,10 @@ namespace Terrain mCompositeMapSceneMgr->clearScene(); } + float Terrain::getHeightAt(const Ogre::Vector3 &worldPos) + { + return mStorage->getHeightAt(worldPos); + } + } diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp index 79e35fd8e..4844e0056 100644 --- a/components/terrain/terrain.hpp +++ b/components/terrain/terrain.hpp @@ -28,16 +28,25 @@ namespace Terrain { public: /// @note takes ownership of \a storage - Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags); + /// @param sceneMgr scene manager to use + /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) + /// @param visbilityFlags visibility flags for the created meshes + /// @param distantLand Whether to draw all of the terrain, or only a 3x3 grid around the camera. + /// This is a temporary option until it can be streamlined. + /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually + /// faster so this is just here for compatibility. + Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags, bool distantLand, bool shaders); ~Terrain(); + bool getDistantLandEnabled() { return mDistantLand; } + bool getShadersEnabled() { return mShaders; } + + float getHeightAt (const Ogre::Vector3& worldPos); + /// Update chunk LODs according to this camera position /// @note Calling this method might lead to composite textures being rendered, so it is best /// not to call it when render commands are still queued, since that would cause a flush. - void update (Ogre::Camera* camera); - - /// \todo - float getHeightAt (const Ogre::Vector3& worldPos) { return 0; } + void update (const Ogre::Vector3& cameraPos); /// Get the world bounding box of a chunk of terrain centered at \a center Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); @@ -63,6 +72,9 @@ namespace Terrain void enableSplattingShader(bool enabled); private: + bool mDistantLand; + bool mShaders; + QuadTreeNode* mRootNode; Storage* mStorage; diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index 1f11217d2..dd016555f 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -160,6 +160,7 @@ @shEndForeach lightResult.xyz += lightAmbient.xyz; lightResult.xyz *= colour.xyz; + directionalResult.xyz *= colour.xyz; @shPassthroughAssign(lightResult, lightResult); @shPassthroughAssign(directionalResult, directionalResult); diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index e4fc9f724..ebfaf678a 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -335,12 +335,12 @@ - - + + - + - + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index ac56604d1..f191430df 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -80,6 +80,7 @@ texture size = 1024 actor shadows = true misc shadows = true statics shadows = true +terrain shadows = true # Fraction of the total shadow distance after which the shadow starts to fade out fade start = 0.8 @@ -125,8 +126,9 @@ fog start factor = 0.5 fog end factor = 1.0 [Terrain] -# Max. number of lights that affect the terrain. Setting to 1 will only reflect sunlight -num lights = 8 +distant land = false + +shader = true [Water] shader = true From b92da9ae93e877d54eb4db9f7f966520759ebad9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 19 Aug 2013 21:08:44 +0200 Subject: [PATCH 043/194] Neighbour fix --- components/terrain/quadtreenode.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index ce94d62a7..007977b0c 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -181,6 +181,10 @@ void QuadTreeNode::initNeighbours() { for (int i=0; i<4; ++i) mNeighbours[i] = searchNeighbourRecursive(this, (Direction)i); + + if (hasChildren()) + for (int i=0; i<4; ++i) + mChildren[i]->initNeighbours(); } void QuadTreeNode::initAabb() From ceb3317807876e95180f1371257fe14ac2a7d54a Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sat, 17 Aug 2013 22:56:50 +0100 Subject: [PATCH 044/194] Integrate unshield with launcher --- CMakeLists.txt | 1 + apps/launcher/CMakeLists.txt | 14 +- apps/launcher/maindialog.cpp | 124 +++++++- apps/launcher/text_slot_msg_box.cpp | 6 + apps/launcher/text_slot_msg_box.hpp | 13 + apps/launcher/unshield_thread.cpp | 441 ++++++++++++++++++++++++++++ apps/launcher/unshield_thread.hpp | 55 ++++ 7 files changed, 648 insertions(+), 6 deletions(-) create mode 100644 apps/launcher/text_slot_msg_box.cpp create mode 100644 apps/launcher/text_slot_msg_box.hpp create mode 100644 apps/launcher/unshield_thread.cpp create mode 100644 apps/launcher/unshield_thread.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 575fecd0c..99a7e85a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -499,6 +499,7 @@ endif(WIN32) add_subdirectory (extern/shiny) add_subdirectory (extern/oics) add_subdirectory (extern/sdl4ogre) +add_subdirectory (extern/unshield) # Components add_subdirectory (components) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index bff26b63c..bfe932448 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -4,6 +4,8 @@ set(LAUNCHER main.cpp maindialog.cpp playpage.cpp + unshield_thread.cpp + text_slot_msg_box.cpp settings/gamesettings.cpp settings/graphicssettings.cpp @@ -20,6 +22,8 @@ set(LAUNCHER_HEADER graphicspage.hpp maindialog.hpp playpage.hpp + unshield_thread.hpp + text_slot_msg_box.hpp settings/gamesettings.hpp settings/graphicssettings.hpp @@ -37,6 +41,8 @@ set(LAUNCHER_HEADER_MOC graphicspage.hpp maindialog.hpp playpage.hpp + unshield_thread.hpp + text_slot_msg_box.hpp utils/checkablemessagebox.hpp utils/textinputdialog.hpp @@ -64,8 +70,11 @@ QT4_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) QT4_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT4_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) + +message(STATUS "hello ${LIBUNSHIELD_INCLUDE}") + include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) # Main executable IF(OGRE_STATIC) @@ -93,8 +102,11 @@ target_link_libraries(omwlauncher ${SDL2_LIBRARY} ${QT_LIBRARIES} components + libunshield ) + + if(DPKG_PROGRAM) INSTALL(TARGETS omwlauncher RUNTIME DESTINATION games COMPONENT omwlauncher) endif() diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index b75d09c51..e274a2896 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -11,6 +11,9 @@ #include +#include "unshield_thread.hpp" +#include "text_slot_msg_box.hpp" + #include "utils/checkablemessagebox.hpp" #include "playpage.hpp" @@ -128,11 +131,16 @@ bool MainDialog::showFirstRunDialog() QDir dir(path); dir.setPath(dir.canonicalPath()); // Resolve symlinks - if (!dir.cdUp()) - continue; // Cannot move from Data Files - if (dir.exists(QString("Morrowind.ini"))) iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + else + { + if (!dir.cdUp()) + continue; // Cannot move from Data Files + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + } } // Ask the user where the Morrowind.ini is @@ -344,6 +352,76 @@ bool MainDialog::setupLauncherSettings() return true; } +bool expansions(UnshieldThread& cd) +{ + if(cd.BloodmoonDone()) + { + cd.Done(); + return false; + } + + QMessageBox expansionsBox; + expansionsBox.setText(QObject::tr("
Would you like to install expansions now ? (make sure you have the disc)
\ + If you want to install both Bloodmoon and Tribunal, you have to install Tribunal first.
")); + + QAbstractButton* tribunalButton = NULL; + if(!cd.TribunalDone()) + tribunalButton = expansionsBox.addButton(QObject::tr("&Tribunal"), QMessageBox::ActionRole); + + QAbstractButton* bloodmoonButton = expansionsBox.addButton(QObject::tr("&Bloodmoon"), QMessageBox::ActionRole); + QAbstractButton* noneButton = expansionsBox.addButton(QObject::tr("&None"), QMessageBox::ActionRole); + + expansionsBox.exec(); + + if(expansionsBox.clickedButton() == noneButton) + { + cd.Done(); + return false; + } + else if(expansionsBox.clickedButton() == tribunalButton) + { + + TextSlotMsgBox cdbox; + cdbox.setStandardButtons(QMessageBox::Cancel); + + QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); + QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); + + cd.SetTribunalPath( + QFileDialog::getOpenFileName( + NULL, + QObject::tr("Select data1.hdr from Tribunal Installation CD (Tribunal/data1.hdr on GOTY CDs)"), + QDir::currentPath(), + QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); + + cd.start(); + cdbox.exec(); + } + else if(expansionsBox.clickedButton() == bloodmoonButton) + { + + TextSlotMsgBox cdbox; + cdbox.setStandardButtons(QMessageBox::Cancel); + + QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); + QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); + + cd.SetBloodmoonPath( + QFileDialog::getOpenFileName( + NULL, + QObject::tr("Select data1.hdr from Bloodmoon Installation CD (Bloodmoon/data1.hdr on GOTY CDs)"), + QDir::currentPath(), + QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); + + cd.start(); + cdbox.exec(); + } + + + + return true; +} + bool MainDialog::setupGameSettings() { QString userPath = QString::fromStdString(mCfgMgr.getUserPath().string()); @@ -401,9 +479,13 @@ bool MainDialog::setupGameSettings() Press \"Browse...\" to specify the location manually.
")); QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); + msgBox.addButton(QObject::tr("Browse to &Install..."), QMessageBox::ActionRole); - msgBox.exec(); + QAbstractButton *cdSelectButton = + msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); + + + msgBox.exec(); QString selectedFile; if (msgBox.clickedButton() == dirSelectButton) { @@ -413,6 +495,38 @@ bool MainDialog::setupGameSettings() QDir::currentPath(), QString(tr("Morrowind master file (*.esm)"))); } + else if(msgBox.clickedButton() == cdSelectButton) { + UnshieldThread cd; + + { + TextSlotMsgBox cdbox; + cdbox.setStandardButtons(QMessageBox::Cancel); + + QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); + QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); + + cd.SetMorrowindPath( + QFileDialog::getOpenFileName( + NULL, + QObject::tr("Select data1.hdr from Morrowind Installation CD"), + QDir::currentPath(), + QString(tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); + + cd.SetOutputPath( + QFileDialog::getExistingDirectory( + NULL, + QObject::tr("Select where to extract files to"), + QDir::currentPath(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks).toUtf8().constData()); + + cd.start(); + cdbox.exec(); + } + + while(expansions(cd)); + + selectedFile = QString::fromStdString(cd.GetMWEsmPath()); + } if (selectedFile.isEmpty()) return false; // Cancel was clicked; diff --git a/apps/launcher/text_slot_msg_box.cpp b/apps/launcher/text_slot_msg_box.cpp new file mode 100644 index 000000000..25bd5d970 --- /dev/null +++ b/apps/launcher/text_slot_msg_box.cpp @@ -0,0 +1,6 @@ +#include "text_slot_msg_box.hpp" + +void TextSlotMsgBox::setTextSlot(const QString& string) +{ + setText(string); +} diff --git a/apps/launcher/text_slot_msg_box.hpp b/apps/launcher/text_slot_msg_box.hpp new file mode 100644 index 000000000..a29e2c354 --- /dev/null +++ b/apps/launcher/text_slot_msg_box.hpp @@ -0,0 +1,13 @@ +#ifndef TEXT_SLOT_MSG_BOX +#define TEXT_SLOT_MSG_BOX + +#include + +class TextSlotMsgBox : public QMessageBox +{ +Q_OBJECT + public slots: + void setTextSlot(const QString& string); +}; + +#endif diff --git a/apps/launcher/unshield_thread.cpp b/apps/launcher/unshield_thread.cpp new file mode 100644 index 000000000..f8166dba8 --- /dev/null +++ b/apps/launcher/unshield_thread.cpp @@ -0,0 +1,441 @@ +#include "unshield_thread.hpp" + +#include +#include + +namespace bfs = boost::filesystem; + + +typedef enum +{ + FORMAT_NEW, + FORMAT_OLD, + FORMAT_RAW +} FORMAT; + + +static bool make_sure_directory_exists(bfs::path directory) +{ + + if(!bfs::exists(directory)) + { + bfs::create_directories(directory); + } + + return bfs::exists(directory); +} + +void fill_path(bfs::path& path, const std::string& name) +{ + size_t start = 0; + + size_t i; + for(i = 0; i < name.length(); i++) + { + switch(name[i]) + { + case '\\': + path /= name.substr(start, i-start); + start = i+1; + break; + } + } + + path /= name.substr(start, i-start); +} + +bool UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) +{ + bool success; + bfs::path dirname; + bfs::path filename; + int directory = unshield_file_directory(unshield, index); + + dirname = output_dir; + + if (prefix && prefix[0]) + dirname /= prefix; + + if (directory >= 0) + { + const char* tmp = unshield_directory_name(unshield, directory); + if (tmp && tmp[0]) + fill_path(dirname, tmp); + } + + make_sure_directory_exists(dirname); + + filename = dirname; + filename /= unshield_file_name(unshield, index); + + emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); + + FORMAT format = FORMAT_NEW; + switch (format) + { + case FORMAT_NEW: + success = unshield_file_save(unshield, index, filename.c_str()); + break; + case FORMAT_OLD: + success = unshield_file_save_old(unshield, index, filename.c_str()); + break; + case FORMAT_RAW: + success = unshield_file_save_raw(unshield, index, filename.c_str()); + break; + } + + if (!success) + bfs::remove(filename); + + return success; +} + +void UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) +{ + Unshield * unshield; + unshield = unshield_open_force_version(cab.c_str(), -1); + + int i; + for (i = 0; i < unshield_file_group_count(unshield); i++) + { + UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); + + for (size_t j = file_group->first_file; j <= file_group->last_file; j++) + { + if (unshield_file_is_valid(unshield, j)) + extract_file(unshield, output_dir, file_group->name, j); + } + } + unshield_close(unshield); +} + +std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) +{ + size_t start = inx.find(category); + start = inx.find(setting, start) + setting.length() + 3; + + size_t end = inx.find("!", start); + + return inx.substr(start, end-start); +} + +std::string read_to_string(const bfs::path& path) +{ + std::ifstream strstream(path.c_str(), std::ios::in | std::ios::binary); + std::string str; + + strstream.seekg(0, std::ios::end); + str.resize(strstream.tellg()); + strstream.seekg(0, std::ios::beg); + strstream.read(&str[0], str.size()); + strstream.close(); + + return str; +} + +void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) +{ + size_t loc; + loc = ini.find("[" + category + "]"); + + // If category is not found, create it + if(loc == std::string::npos) + { + loc = ini.size() + 2; + ini += ("\r\n[" + category + "]\r\n"); + } + + loc += category.length() +2 +2; + ini.insert(loc, setting + "=" + val + "\r\n"); +} + +void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) +{ + std::string inx = read_to_string(inxPath); + + // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) + size_t start = ini.find("[Weather Blight]"); + start = ini.find("Ambient Loop Sound ID", start); + size_t end = ini.find("\r\n", start) +2; + ini.erase(start, end-start); + + std::string category; + std::string setting; + + category = "General"; + { + setting = "Werewolf FOV"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Moons"; + { + setting = "Script Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather"; + { + setting = "Snow Ripples"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Radius"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripples Per Flake"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Gravity Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow High Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Low Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Blight"; + { + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Snow"; + { + setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Diameter"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Height Min"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Height Max"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Entrance Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Max Snowflakes"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Blizzard"; + { + setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Storm Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } +} + + +void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) +{ + bfs::path ini_path = output_dir; + ini_path /= "Morrowind.ini"; + + std::string ini = read_to_string(ini_path.string()); + + if(tribunal) + { + add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); + add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); + } + if(bloodmoon) + { + bloodmoon_fix_ini(ini, cdPath / "setup.inx"); + add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); + add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); + } + + std::ofstream inistream(ini_path.c_str()); + inistream << ini; + inistream.close(); +} + +bool UnshieldThread::SetMorrowindPath(const std::string& path) +{ + mMorrowindPath = path; + return true; +} + +bool UnshieldThread::SetTribunalPath(const std::string& path) +{ + mTribunalPath = path; + return true; +} + +bool UnshieldThread::SetBloodmoonPath(const std::string& path) +{ + mBloodmoonPath = path; + return true; +} + +void UnshieldThread::SetOutputPath(const std::string& path) +{ + mOutputPath = path; +} + +void installToPath(const bfs::path& from, const bfs::path& to) +{ + make_sure_directory_exists(to); + + for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) + { + if(bfs::is_directory(dir->path())) + installToPath(dir->path(), to / dir->path().filename()); + else + bfs::rename(dir->path(), to / dir->path().filename()); + } +} + +bfs::path findFile(const bfs::path& in, std::string filename) +{ + for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return dir->path(); + } + + return ""; +} + +bool contains(const bfs::path& in, std::string filename) +{ + for(bfs::directory_iterator end, dir(in); dir != end; ++dir) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return true; + } + + return false; +} + +bool UnshieldThread::extract() +{ + bfs::path outputDataFilesDir = mOutputPath; + outputDataFilesDir /= "Data Files"; + bfs::path extractPath = mOutputPath; + extractPath /= "extract-temp"; + + if(!mMorrowindDone && mMorrowindPath.string().length() > 0) + { + mMorrowindDone = true; + + bfs::path mwExtractPath = extractPath / "morrowind"; + extract_cab(mMorrowindPath, mwExtractPath, true); + + bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); + + + installToPath(dFilesDir, outputDataFilesDir); + + bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); + + mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); + mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + + } + + else if(!mTribunalDone && mTribunalPath.string().length() > 0) + { + mTribunalDone = true; + + bfs::path tbExtractPath = extractPath / "tribunal"; + extract_cab(mTribunalPath, tbExtractPath, true); + + bfs::path dFilesDir = findFile(tbExtractPath, "tribunal.esm").parent_path(); + + installToPath(dFilesDir, outputDataFilesDir); + + mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + + fix_ini(outputDataFilesDir, bfs::path(mTribunalPath).parent_path(), mTribunalDone, mBloodmoonDone); + } + + else if(!mBloodmoonDone && mBloodmoonPath.string().length() > 0) + { + mBloodmoonDone = true; + + bfs::path bmExtractPath = extractPath / "bloodmoon"; + extract_cab(mBloodmoonPath, bmExtractPath, true); + + bfs::path dFilesDir = findFile(bmExtractPath, "bloodmoon.esm").parent_path(); + + installToPath(dFilesDir, outputDataFilesDir); + + fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); + } + + + return true; +} + +time_t getTime(const char* time) +{ + struct tm tms; + memset(&tms, 0, sizeof(struct tm)); + strptime(time, "%d %B %Y", &tms); + return mktime(&tms); +} + +void UnshieldThread::Done() +{ + // Get rid of unnecessary files + bfs::remove_all(mOutputPath / "extract-temp"); + + // Set modified time to release dates, to preserve load order + if(mMorrowindDone) + bfs::last_write_time(findFile(mOutputPath, "morrowind.esm"), getTime("1 May 2002")); + + if(mTribunalDone) + bfs::last_write_time(findFile(mOutputPath, "tribunal.esm"), getTime("6 November 2002")); + + if(mBloodmoonDone) + bfs::last_write_time(findFile(mOutputPath, "bloodmoon.esm"), getTime("3 June 2003")); +} + +std::string UnshieldThread::GetMWEsmPath() +{ + return findFile(mOutputPath / "Data Files", "morrowind.esm").string(); +} + +bool UnshieldThread::TribunalDone() +{ + return mTribunalDone; +} + +bool UnshieldThread::BloodmoonDone() +{ + return mBloodmoonDone; +} + +void UnshieldThread::run() +{ + extract(); + emit close(); +} diff --git a/apps/launcher/unshield_thread.hpp b/apps/launcher/unshield_thread.hpp new file mode 100644 index 000000000..0def3ab1d --- /dev/null +++ b/apps/launcher/unshield_thread.hpp @@ -0,0 +1,55 @@ +#ifndef UNSHIELD_THREAD_H +#define UNSHIELD_THREAD_H + +#include + +#include + +#include + + +class UnshieldThread : public QThread +{ + Q_OBJECT + + public: + bool SetMorrowindPath(const std::string& path); + bool SetTribunalPath(const std::string& path); + bool SetBloodmoonPath(const std::string& path); + + void SetOutputPath(const std::string& path); + + bool extract(); + + bool TribunalDone(); + bool BloodmoonDone(); + + void Done(); + + std::string GetMWEsmPath(); + + private: + + void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); + bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); + + boost::filesystem::path mMorrowindPath; + boost::filesystem::path mTribunalPath; + boost::filesystem::path mBloodmoonPath; + + bool mMorrowindDone = false; + bool mTribunalDone = false; + bool mBloodmoonDone = false; + + boost::filesystem::path mOutputPath; + + + protected: + virtual void run(); + + signals: + void signalGUI(QString); + void close(); +}; + +#endif From 90a892d3041eb0727794395776101ceaff42a0e5 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 00:16:20 +0100 Subject: [PATCH 045/194] unshield fixes --- apps/launcher/unshield_thread.cpp | 48 ++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/launcher/unshield_thread.cpp b/apps/launcher/unshield_thread.cpp index f8166dba8..57ba849dc 100644 --- a/apps/launcher/unshield_thread.cpp +++ b/apps/launcher/unshield_thread.cpp @@ -300,7 +300,7 @@ void UnshieldThread::SetOutputPath(const std::string& path) mOutputPath = path; } -void installToPath(const bfs::path& from, const bfs::path& to) +void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) { make_sure_directory_exists(to); @@ -309,16 +309,32 @@ void installToPath(const bfs::path& from, const bfs::path& to) if(bfs::is_directory(dir->path())) installToPath(dir->path(), to / dir->path().filename()); else - bfs::rename(dir->path(), to / dir->path().filename()); + { + if(!copy) + bfs::rename(dir->path(), to / dir->path().filename()); + else + bfs::copy_file(dir->path(), to / dir->path().filename()); + } } } -bfs::path findFile(const bfs::path& in, std::string filename) +bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + if(recursive) { - if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) - return dir->path(); + for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return dir->path(); + } + } + else + { + for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return dir->path(); + } } return ""; @@ -350,10 +366,17 @@ bool UnshieldThread::extract() extract_cab(mMorrowindPath, mwExtractPath, true); bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); - installToPath(dFilesDir, outputDataFilesDir); + // Videos are often kept uncompressed on the cd + bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); + if(videosPath.string() != "") + { + emit signalGUI(QString("Installing Videos...")); + installToPath(videosPath, outputDataFilesDir / "Video", true); + } + bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); @@ -371,6 +394,11 @@ bool UnshieldThread::extract() bfs::path dFilesDir = findFile(tbExtractPath, "tribunal.esm").parent_path(); installToPath(dFilesDir, outputDataFilesDir); + + // Mt GOTY CD has Sounds in a seperate folder from the rest of the data files + bfs::path soundsPath = findFile(tbExtractPath, "sounds", false); + if(soundsPath.string() != "") + installToPath(soundsPath, outputDataFilesDir / "Sounds"); mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); @@ -387,6 +415,12 @@ bool UnshieldThread::extract() bfs::path dFilesDir = findFile(bmExtractPath, "bloodmoon.esm").parent_path(); installToPath(dFilesDir, outputDataFilesDir); + + // My GOTY CD contains a folder within cab files called Tribunal patch, + // which contains Tribunal.esm + bfs::path tbPatchPath = findFile(bmExtractPath, "tribunal.esm"); + if(tbPatchPath.string() != "") + bfs::rename(tbPatchPath, outputDataFilesDir / "Tribunal.esm"); fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); } From 3264b5974e6a6060fb95b67aac00984ca879bc21 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 00:29:46 +0100 Subject: [PATCH 046/194] fix invalid syntax --- apps/launcher/unshield_thread.cpp | 7 +++++++ apps/launcher/unshield_thread.hpp | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/launcher/unshield_thread.cpp b/apps/launcher/unshield_thread.cpp index 57ba849dc..34b6a6d46 100644 --- a/apps/launcher/unshield_thread.cpp +++ b/apps/launcher/unshield_thread.cpp @@ -473,3 +473,10 @@ void UnshieldThread::run() extract(); emit close(); } + +UnshieldThread::UnshieldThread() +{ + mMorrowindDone = false; + mTribunalDone = false; + mBloodmoonDone = false; +} diff --git a/apps/launcher/unshield_thread.hpp b/apps/launcher/unshield_thread.hpp index 0def3ab1d..b48d3d987 100644 --- a/apps/launcher/unshield_thread.hpp +++ b/apps/launcher/unshield_thread.hpp @@ -28,6 +28,8 @@ class UnshieldThread : public QThread std::string GetMWEsmPath(); + UnshieldThread(); + private: void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); @@ -37,9 +39,9 @@ class UnshieldThread : public QThread boost::filesystem::path mTribunalPath; boost::filesystem::path mBloodmoonPath; - bool mMorrowindDone = false; - bool mTribunalDone = false; - bool mBloodmoonDone = false; + bool mMorrowindDone; + bool mTribunalDone; + bool mBloodmoonDone; boost::filesystem::path mOutputPath; From 454b64974d56cb5aaa7b70cbc73de00219521287 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 12:11:39 +0100 Subject: [PATCH 047/194] filenames --- apps/launcher/CMakeLists.txt | 12 ++++++------ apps/launcher/maindialog.cpp | 4 ++-- .../{text_slot_msg_box.cpp => textslotmsgbox.cpp} | 2 +- .../{text_slot_msg_box.hpp => textslotmsgbox.hpp} | 0 .../{unshield_thread.cpp => unshieldthread.cpp} | 2 +- .../{unshield_thread.hpp => unshieldthread.hpp} | 0 6 files changed, 10 insertions(+), 10 deletions(-) rename apps/launcher/{text_slot_msg_box.cpp => textslotmsgbox.cpp} (71%) rename apps/launcher/{text_slot_msg_box.hpp => textslotmsgbox.hpp} (100%) rename apps/launcher/{unshield_thread.cpp => unshieldthread.cpp} (99%) rename apps/launcher/{unshield_thread.hpp => unshieldthread.hpp} (100%) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index bfe932448..e11b713bc 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -4,8 +4,8 @@ set(LAUNCHER main.cpp maindialog.cpp playpage.cpp - unshield_thread.cpp - text_slot_msg_box.cpp + unshieldthread.cpp + textslotmsgbox.cpp settings/gamesettings.cpp settings/graphicssettings.cpp @@ -22,8 +22,8 @@ set(LAUNCHER_HEADER graphicspage.hpp maindialog.hpp playpage.hpp - unshield_thread.hpp - text_slot_msg_box.hpp + unshieldthread.hpp + textslotmsgbox.hpp settings/gamesettings.hpp settings/graphicssettings.hpp @@ -41,8 +41,8 @@ set(LAUNCHER_HEADER_MOC graphicspage.hpp maindialog.hpp playpage.hpp - unshield_thread.hpp - text_slot_msg_box.hpp + unshieldthread.hpp + textslotmsgbox.hpp utils/checkablemessagebox.hpp utils/textinputdialog.hpp diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index e274a2896..857a15196 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -11,8 +11,8 @@ #include -#include "unshield_thread.hpp" -#include "text_slot_msg_box.hpp" +#include "unshieldthread.hpp" +#include "textslotmsgbox.hpp" #include "utils/checkablemessagebox.hpp" diff --git a/apps/launcher/text_slot_msg_box.cpp b/apps/launcher/textslotmsgbox.cpp similarity index 71% rename from apps/launcher/text_slot_msg_box.cpp rename to apps/launcher/textslotmsgbox.cpp index 25bd5d970..0607d1cc6 100644 --- a/apps/launcher/text_slot_msg_box.cpp +++ b/apps/launcher/textslotmsgbox.cpp @@ -1,4 +1,4 @@ -#include "text_slot_msg_box.hpp" +#include "textslotmsgbox.hpp" void TextSlotMsgBox::setTextSlot(const QString& string) { diff --git a/apps/launcher/text_slot_msg_box.hpp b/apps/launcher/textslotmsgbox.hpp similarity index 100% rename from apps/launcher/text_slot_msg_box.hpp rename to apps/launcher/textslotmsgbox.hpp diff --git a/apps/launcher/unshield_thread.cpp b/apps/launcher/unshieldthread.cpp similarity index 99% rename from apps/launcher/unshield_thread.cpp rename to apps/launcher/unshieldthread.cpp index 34b6a6d46..203302388 100644 --- a/apps/launcher/unshield_thread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -1,4 +1,4 @@ -#include "unshield_thread.hpp" +#include "unshieldthread.hpp" #include #include diff --git a/apps/launcher/unshield_thread.hpp b/apps/launcher/unshieldthread.hpp similarity index 100% rename from apps/launcher/unshield_thread.hpp rename to apps/launcher/unshieldthread.hpp From 641b7b0336ff95f56493883d424f132bbdb56072 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 12:23:09 +0100 Subject: [PATCH 048/194] anonymous namespace --- apps/launcher/unshieldthread.cpp | 547 ++++++++++++++++--------------- 1 file changed, 275 insertions(+), 272 deletions(-) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index 203302388..10470dab9 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -5,276 +5,272 @@ namespace bfs = boost::filesystem; +namespace +{ + typedef enum + { + FORMAT_NEW, + FORMAT_OLD, + FORMAT_RAW + } FORMAT; -typedef enum -{ - FORMAT_NEW, - FORMAT_OLD, - FORMAT_RAW -} FORMAT; + static bool make_sure_directory_exists(bfs::path directory) + { -static bool make_sure_directory_exists(bfs::path directory) -{ + if(!bfs::exists(directory)) + { + bfs::create_directories(directory); + } - if(!bfs::exists(directory)) - { - bfs::create_directories(directory); + return bfs::exists(directory); } - return bfs::exists(directory); -} - -void fill_path(bfs::path& path, const std::string& name) -{ - size_t start = 0; - - size_t i; - for(i = 0; i < name.length(); i++) + void fill_path(bfs::path& path, const std::string& name) { - switch(name[i]) + size_t start = 0; + + size_t i; + for(i = 0; i < name.length(); i++) { - case '\\': - path /= name.substr(start, i-start); - start = i+1; - break; + switch(name[i]) + { + case '\\': + path /= name.substr(start, i-start); + start = i+1; + break; + } } - } - path /= name.substr(start, i-start); -} - -bool UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) -{ - bool success; - bfs::path dirname; - bfs::path filename; - int directory = unshield_file_directory(unshield, index); - - dirname = output_dir; - - if (prefix && prefix[0]) - dirname /= prefix; + path /= name.substr(start, i-start); + } - if (directory >= 0) + std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) { - const char* tmp = unshield_directory_name(unshield, directory); - if (tmp && tmp[0]) - fill_path(dirname, tmp); - } + size_t start = inx.find(category); + start = inx.find(setting, start) + setting.length() + 3; - make_sure_directory_exists(dirname); + size_t end = inx.find("!", start); - filename = dirname; - filename /= unshield_file_name(unshield, index); - - emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); - - FORMAT format = FORMAT_NEW; - switch (format) - { - case FORMAT_NEW: - success = unshield_file_save(unshield, index, filename.c_str()); - break; - case FORMAT_OLD: - success = unshield_file_save_old(unshield, index, filename.c_str()); - break; - case FORMAT_RAW: - success = unshield_file_save_raw(unshield, index, filename.c_str()); - break; + return inx.substr(start, end-start); } - if (!success) - bfs::remove(filename); + std::string read_to_string(const bfs::path& path) + { + std::ifstream strstream(path.c_str(), std::ios::in | std::ios::binary); + std::string str; - return success; -} + strstream.seekg(0, std::ios::end); + str.resize(strstream.tellg()); + strstream.seekg(0, std::ios::beg); + strstream.read(&str[0], str.size()); + strstream.close(); -void UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) -{ - Unshield * unshield; - unshield = unshield_open_force_version(cab.c_str(), -1); - - int i; - for (i = 0; i < unshield_file_group_count(unshield); i++) - { - UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); + return str; + } - for (size_t j = file_group->first_file; j <= file_group->last_file; j++) + void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) + { + size_t loc; + loc = ini.find("[" + category + "]"); + + // If category is not found, create it + if(loc == std::string::npos) { - if (unshield_file_is_valid(unshield, j)) - extract_file(unshield, output_dir, file_group->name, j); + loc = ini.size() + 2; + ini += ("\r\n[" + category + "]\r\n"); } + + loc += category.length() +2 +2; + ini.insert(loc, setting + "=" + val + "\r\n"); } - unshield_close(unshield); -} -std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) -{ - size_t start = inx.find(category); - start = inx.find(setting, start) + setting.length() + 3; + void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) + { + std::string inx = read_to_string(inxPath); - size_t end = inx.find("!", start); + // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) + size_t start = ini.find("[Weather Blight]"); + start = ini.find("Ambient Loop Sound ID", start); + size_t end = ini.find("\r\n", start) +2; + ini.erase(start, end-start); - return inx.substr(start, end-start); -} + std::string category; + std::string setting; -std::string read_to_string(const bfs::path& path) -{ - std::ifstream strstream(path.c_str(), std::ios::in | std::ios::binary); - std::string str; - - strstream.seekg(0, std::ios::end); - str.resize(strstream.tellg()); - strstream.seekg(0, std::ios::beg); - strstream.read(&str[0], str.size()); - strstream.close(); + category = "General"; + { + setting = "Werewolf FOV"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Moons"; + { + setting = "Script Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather"; + { + setting = "Snow Ripples"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Radius"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripples Per Flake"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Ripple Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Gravity Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow High Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Low Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Blight"; + { + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Snow"; + { + setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Diameter"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Height Min"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Height Max"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Snow Entrance Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Max Snowflakes"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + category = "Weather Blizzard"; + { + setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); + setting = "Storm Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); + } + } - return str; -} -void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) -{ - size_t loc; - loc = ini.find("[" + category + "]"); - - // If category is not found, create it - if(loc == std::string::npos) + void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) { - loc = ini.size() + 2; - ini += ("\r\n[" + category + "]\r\n"); - } - - loc += category.length() +2 +2; - ini.insert(loc, setting + "=" + val + "\r\n"); -} - -void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) -{ - std::string inx = read_to_string(inxPath); + bfs::path ini_path = output_dir; + ini_path /= "Morrowind.ini"; - // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) - size_t start = ini.find("[Weather Blight]"); - start = ini.find("Ambient Loop Sound ID", start); - size_t end = ini.find("\r\n", start) +2; - ini.erase(start, end-start); + std::string ini = read_to_string(ini_path.string()); - std::string category; - std::string setting; + if(tribunal) + { + add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); + add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); + } + if(bloodmoon) + { + bloodmoon_fix_ini(ini, cdPath / "setup.inx"); + add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); + add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); + } - category = "General"; - { - setting = "Werewolf FOV"; add_setting(category, setting, get_setting(category, setting, inx), ini); + std::ofstream inistream(ini_path.c_str()); + inistream << ini; + inistream.close(); } - category = "Moons"; - { - setting = "Script Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - } - category = "Weather"; - { - setting = "Snow Ripples"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Ripple Radius"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Ripples Per Flake"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Ripple Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Ripple Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Gravity Scale"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow High Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Low Kill"; add_setting(category, setting, get_setting(category, setting, inx), ini); - } - category = "Weather Blight"; - { - setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); - } - category = "Weather Snow"; - { - setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Diameter"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Height Min"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Height Max"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Snow Entrance Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Max Snowflakes"; add_setting(category, setting, get_setting(category, setting, inx), ini); - } - category = "Weather Blizzard"; + + void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) { - setting = "Sky Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sky Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Fog Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Sunrise Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Day Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Night Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Sun Disc Sunset Color"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Transition Delta"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Land Fog Day Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Land Fog Night Depth"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Clouds Maximum Percent"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Wind Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Cloud Speed"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Glare View"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Cloud Texture"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Ambient Loop Sound ID"; add_setting(category, setting, get_setting(category, setting, inx), ini); - setting = "Storm Threshold"; add_setting(category, setting, get_setting(category, setting, inx), ini); - } -} + make_sure_directory_exists(to); + for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) + { + if(bfs::is_directory(dir->path())) + installToPath(dir->path(), to / dir->path().filename()); + else + { + if(!copy) + bfs::rename(dir->path(), to / dir->path().filename()); + else + bfs::copy_file(dir->path(), to / dir->path().filename()); + } + } + } -void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) -{ - bfs::path ini_path = output_dir; - ini_path /= "Morrowind.ini"; + bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) + { + if(recursive) + { + for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return dir->path(); + } + } + else + { + for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return dir->path(); + } + } - std::string ini = read_to_string(ini_path.string()); + return ""; + } - if(tribunal) + bool contains(const bfs::path& in, std::string filename) { - add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); - add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); + for(bfs::directory_iterator end, dir(in); dir != end; ++dir) + { + if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) + return true; + } + + return false; } - if(bloodmoon) + + time_t getTime(const char* time) { - bloodmoon_fix_ini(ini, cdPath / "setup.inx"); - add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); - add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); + struct tm tms; + memset(&tms, 0, sizeof(struct tm)); + strptime(time, "%d %B %Y", &tms); + return mktime(&tms); } - - std::ofstream inistream(ini_path.c_str()); - inistream << ini; - inistream.close(); } bool UnshieldThread::SetMorrowindPath(const std::string& path) @@ -300,57 +296,72 @@ void UnshieldThread::SetOutputPath(const std::string& path) mOutputPath = path; } -void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) +bool UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) { - make_sure_directory_exists(to); + bool success; + bfs::path dirname; + bfs::path filename; + int directory = unshield_file_directory(unshield, index); - for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) - { - if(bfs::is_directory(dir->path())) - installToPath(dir->path(), to / dir->path().filename()); - else - { - if(!copy) - bfs::rename(dir->path(), to / dir->path().filename()); - else - bfs::copy_file(dir->path(), to / dir->path().filename()); - } - } -} + dirname = output_dir; -bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) -{ - if(recursive) + if (prefix && prefix[0]) + dirname /= prefix; + + if (directory >= 0) { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) - return dir->path(); - } + const char* tmp = unshield_directory_name(unshield, directory); + if (tmp && tmp[0]) + fill_path(dirname, tmp); } - else + + make_sure_directory_exists(dirname); + + filename = dirname; + filename /= unshield_file_name(unshield, index); + + emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); + + FORMAT format = FORMAT_NEW; + switch (format) { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) - return dir->path(); - } + case FORMAT_NEW: + success = unshield_file_save(unshield, index, filename.c_str()); + break; + case FORMAT_OLD: + success = unshield_file_save_old(unshield, index, filename.c_str()); + break; + case FORMAT_RAW: + success = unshield_file_save_raw(unshield, index, filename.c_str()); + break; } - return ""; + if (!success) + bfs::remove(filename); + + return success; } -bool contains(const bfs::path& in, std::string filename) +void UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) { - for(bfs::directory_iterator end, dir(in); dir != end; ++dir) + Unshield * unshield; + unshield = unshield_open_force_version(cab.c_str(), -1); + + int i; + for (i = 0; i < unshield_file_group_count(unshield); i++) { - if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) - return true; - } + UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); - return false; + for (size_t j = file_group->first_file; j <= file_group->last_file; j++) + { + if (unshield_file_is_valid(unshield, j)) + extract_file(unshield, output_dir, file_group->name, j); + } + } + unshield_close(unshield); } + bool UnshieldThread::extract() { bfs::path outputDataFilesDir = mOutputPath; @@ -429,14 +440,6 @@ bool UnshieldThread::extract() return true; } -time_t getTime(const char* time) -{ - struct tm tms; - memset(&tms, 0, sizeof(struct tm)); - strptime(time, "%d %B %Y", &tms); - return mktime(&tms); -} - void UnshieldThread::Done() { // Get rid of unnecessary files From 9d1daf7dc2e9be1d6375abf931aafe63d01de9f2 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 12:29:12 +0100 Subject: [PATCH 049/194] enum was unnecessary --- apps/launcher/unshieldthread.cpp | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index 10470dab9..4ccb6b04e 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -7,14 +7,6 @@ namespace bfs = boost::filesystem; namespace { - typedef enum - { - FORMAT_NEW, - FORMAT_OLD, - FORMAT_RAW - } FORMAT; - - static bool make_sure_directory_exists(bfs::path directory) { @@ -322,19 +314,7 @@ bool UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, cons emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); - FORMAT format = FORMAT_NEW; - switch (format) - { - case FORMAT_NEW: - success = unshield_file_save(unshield, index, filename.c_str()); - break; - case FORMAT_OLD: - success = unshield_file_save_old(unshield, index, filename.c_str()); - break; - case FORMAT_RAW: - success = unshield_file_save_raw(unshield, index, filename.c_str()); - break; - } + success = unshield_file_save(unshield, index, filename.c_str()); if (!success) bfs::remove(filename); From d3748cd5bb8b8c7be40ca2d843346fff5ee587c1 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sun, 18 Aug 2013 12:46:10 +0100 Subject: [PATCH 050/194] Install uncompressed data files from cd --- apps/launcher/unshieldthread.cpp | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index 4ccb6b04e..5a9475d96 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -212,13 +212,13 @@ namespace for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) { if(bfs::is_directory(dir->path())) - installToPath(dir->path(), to / dir->path().filename()); + installToPath(dir->path(), to / dir->path().filename(), copy); else { - if(!copy) - bfs::rename(dir->path(), to / dir->path().filename()); - else + if(copy) bfs::copy_file(dir->path(), to / dir->path().filename()); + else + bfs::rename(dir->path(), to / dir->path().filename()); } } } @@ -235,7 +235,7 @@ namespace } else { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) + for ( bfs::directory_iterator end, dir(in); dir != end; ++dir ) { if(Misc::StringUtils::lowerCase(dir->path().filename().string()) == filename) return dir->path(); @@ -368,6 +368,14 @@ bool UnshieldThread::extract() installToPath(videosPath, outputDataFilesDir / "Video", true); } + bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); + if(cdDFiles.string() != "") + { + emit signalGUI(QString("Installing Uncompressed Data files from CD...")); + installToPath(cdDFiles, outputDataFilesDir, true); + } + + bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); @@ -391,6 +399,13 @@ bool UnshieldThread::extract() if(soundsPath.string() != "") installToPath(soundsPath, outputDataFilesDir / "Sounds"); + bfs::path cdDFiles = findFile(mTribunalPath.parent_path(), "data files", false); + if(cdDFiles.string() != "") + { + emit signalGUI(QString("Installing Uncompressed Data files from CD...")); + installToPath(cdDFiles, outputDataFilesDir, true); + } + mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); fix_ini(outputDataFilesDir, bfs::path(mTribunalPath).parent_path(), mTribunalDone, mBloodmoonDone); @@ -412,6 +427,13 @@ bool UnshieldThread::extract() bfs::path tbPatchPath = findFile(bmExtractPath, "tribunal.esm"); if(tbPatchPath.string() != "") bfs::rename(tbPatchPath, outputDataFilesDir / "Tribunal.esm"); + + bfs::path cdDFiles = findFile(mBloodmoonPath.parent_path(), "data files", false); + if(cdDFiles.string() != "") + { + emit signalGUI(QString("Installing Uncompressed Data files from CD...")); + installToPath(cdDFiles, outputDataFilesDir, true); + } fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); } From 865a7c63df278214f2585804374089ad126da271 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Mon, 19 Aug 2013 19:59:58 +0100 Subject: [PATCH 051/194] cmake for system libunshield --- CMakeLists.txt | 8 +++++- apps/launcher/CMakeLists.txt | 4 +-- cmake/FindLibUnshield.cmake | 48 ++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 cmake/FindLibUnshield.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 99a7e85a4..148a56d3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -499,7 +499,6 @@ endif(WIN32) add_subdirectory (extern/shiny) add_subdirectory (extern/oics) add_subdirectory (extern/sdl4ogre) -add_subdirectory (extern/unshield) # Components add_subdirectory (components) @@ -516,6 +515,13 @@ if (BUILD_ESMTOOL) endif() if (BUILD_LAUNCHER) + if(NOT WIN32) + find_package(LIBUNSHIELD REQUIRED) + if(NOT LIBUNSHIELD_FOUND) + message(SEND_ERROR "Failed to find libunshield") + endif(NOT LIBUNSHIELD_FOUND) + endif(NOT WIN32) + add_subdirectory( apps/launcher ) endif() diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index e11b713bc..9b812e4ba 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -71,8 +71,6 @@ QT4_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT4_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) -message(STATUS "hello ${LIBUNSHIELD_INCLUDE}") - include(${QT_USE_FILE}) include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) @@ -102,7 +100,7 @@ target_link_libraries(omwlauncher ${SDL2_LIBRARY} ${QT_LIBRARIES} components - libunshield + ${LIBUNSHIELD_LIBRARY} ) diff --git a/cmake/FindLibUnshield.cmake b/cmake/FindLibUnshield.cmake new file mode 100644 index 000000000..4f4e98a1c --- /dev/null +++ b/cmake/FindLibUnshield.cmake @@ -0,0 +1,48 @@ +# Locate LIBUNSHIELD +# This module defines +# LIBUNSHIELD_LIBRARY +# LIBUNSHIELD_FOUND, if false, do not try to link to LibUnshield +# LIBUNSHIELD_INCLUDE_DIR, where to find the headers +# +# Created by Tom Mason (wheybags) for OpenMW (http://openmw.com), based on FindMPG123.cmake +# +# Ripped off from other sources. In fact, this file is so generic (I +# just did a search and replace on another file) that I wonder why the +# CMake guys haven't wrapped this entire thing in a single +# function. Do we really need to repeat this stuff for every single +# library when they all work the same? + +FIND_PATH(LIBUNSHIELD_INCLUDE_DIR libunshield.h + HINTS + PATHS + ~/Library/Frameworks + /Library/Frameworks + /usr/local + /usr + /sw # Fink + /opt/local # DarwinPorts + /opt/csw # Blastwave + /opt + /usr/include +) + +FIND_LIBRARY(LIBUNSHIELD_LIBRARY + unshield + HINTS +# PATH_SUFFIXES lib64 lib libs64 libs libs/Win32 libs/Win64 + PATHS + ~/Library/Frameworks + /Library/Frameworks + /usr/local + /usr + /sw + /opt/local + /opt/csw + /opt + /usr/lib +) + +IF(LIBUNSHIELD_LIBRARY AND LIBUNSHIELD_INCLUDE_DIR) + SET(LIBUNSHIELD_FOUND "YES") +ENDIF(LIBUNSHIELD_LIBRARY AND LIBUNSHIELD_INCLUDE_DIR) + From 06ff40eda731a657658c5ba930fd514f36aaf94f Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Mon, 19 Aug 2013 20:11:52 +0100 Subject: [PATCH 052/194] only use unshield on not windows --- apps/launcher/CMakeLists.txt | 24 +++++++++++++++++++++--- apps/launcher/maindialog.cpp | 17 +++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 9b812e4ba..5908deb90 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -4,7 +4,6 @@ set(LAUNCHER main.cpp maindialog.cpp playpage.cpp - unshieldthread.cpp textslotmsgbox.cpp settings/gamesettings.cpp @@ -16,6 +15,9 @@ set(LAUNCHER ${CMAKE_SOURCE_DIR}/files/launcher/launcher.rc ) +if(NOT WIN32) + LIST(APPEND LAUNCHER unshieldthread.cpp) +endif(NOT WIN32) set(LAUNCHER_HEADER datafilespage.hpp @@ -34,6 +36,10 @@ set(LAUNCHER_HEADER utils/textinputdialog.hpp ) +if(NOT WIN32) + LIST(APPEND LAUNCHER_HEADER unshieldthread.hpp) +endif(NOT WIN32) + # Headers that must be pre-processed set(LAUNCHER_HEADER_MOC @@ -48,6 +54,11 @@ set(LAUNCHER_HEADER_MOC utils/textinputdialog.hpp ) +if(NOT WIN32) + LIST(APPEND LAUNCHER_HEADER_MOC unshieldthread.hpp) +endif(NOT WIN32) + + set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -72,7 +83,10 @@ QT4_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +if(NOT WIN32) + include_directories(${LIBUNSHIELD_INCLUDE}) +endif(NOT WIN32) # Main executable IF(OGRE_STATIC) @@ -100,8 +114,12 @@ target_link_libraries(omwlauncher ${SDL2_LIBRARY} ${QT_LIBRARIES} components - ${LIBUNSHIELD_LIBRARY} ) +if(NOT WIN32) + target_link_libraries(omwlauncher + ${LIBUNSHIELD_LIBRARY} + ) +endif(NOT WIN32) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 857a15196..032f70916 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -11,7 +11,10 @@ #include -#include "unshieldthread.hpp" +#ifndef WIN32 + #include "unshieldthread.hpp" +#endif + #include "textslotmsgbox.hpp" #include "utils/checkablemessagebox.hpp" @@ -352,6 +355,7 @@ bool MainDialog::setupLauncherSettings() return true; } +#ifndef WIN32 bool expansions(UnshieldThread& cd) { if(cd.BloodmoonDone()) @@ -421,6 +425,7 @@ bool expansions(UnshieldThread& cd) return true; } +#endif // WIN32 bool MainDialog::setupGameSettings() { @@ -480,9 +485,11 @@ bool MainDialog::setupGameSettings() QAbstractButton *dirSelectButton = msgBox.addButton(QObject::tr("Browse to &Install..."), QMessageBox::ActionRole); - - QAbstractButton *cdSelectButton = - msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); + + #ifndef WIN32 + QAbstractButton *cdSelectButton = + msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); + #endif msgBox.exec(); @@ -495,6 +502,7 @@ bool MainDialog::setupGameSettings() QDir::currentPath(), QString(tr("Morrowind master file (*.esm)"))); } + #ifndef WIN32 else if(msgBox.clickedButton() == cdSelectButton) { UnshieldThread cd; @@ -527,6 +535,7 @@ bool MainDialog::setupGameSettings() selectedFile = QString::fromStdString(cd.GetMWEsmPath()); } + #endif // WIN32 if (selectedFile.isEmpty()) return false; // Cancel was clicked; From 603ce4105474f0ae4864bed8d6e25538d53a67a9 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Mon, 19 Aug 2013 20:32:19 +0100 Subject: [PATCH 053/194] added libunshield to travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5c69f49f0..caf2e3389 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_install: - sudo apt-get install -qq libqt4-dev libxaw7-dev libxrandr-dev libfreeimage-dev libpng-dev - sudo apt-get install -qq libopenal-dev libmpg123-dev libsndfile1-dev - sudo apt-get install -qq libavcodec-dev libavformat-dev libavdevice-dev libavutil-dev libswscale-dev libpostproc-dev - - sudo apt-get install -qq libbullet-dev libogre-static-dev libmygui-static-dev libsdl2-static-dev + - sudo apt-get install -qq libbullet-dev libogre-static-dev libmygui-static-dev libsdl2-static-dev libunshield-dev - sudo mkdir /usr/src/gtest/build - cd /usr/src/gtest/build - sudo cmake .. -DBUILD_SHARED_LIBS=1 From 8d232aca35c679ae48b4402b77462aed51a131c7 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Mon, 19 Aug 2013 20:48:20 +0100 Subject: [PATCH 054/194] changed libunshield filename to satisfy travis --- cmake/{FindLibUnshield.cmake => FindLIBUNSHIELD.cmake} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmake/{FindLibUnshield.cmake => FindLIBUNSHIELD.cmake} (100%) diff --git a/cmake/FindLibUnshield.cmake b/cmake/FindLIBUNSHIELD.cmake similarity index 100% rename from cmake/FindLibUnshield.cmake rename to cmake/FindLIBUNSHIELD.cmake From 13afcc932473d745d77eb95f07792e19a8769996 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 19 Aug 2013 22:22:14 +0200 Subject: [PATCH 055/194] Don't link to OgreTerrain --- CMakeLists.txt | 1 - apps/openmw/CMakeLists.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd58ed416..9e5e5fcc1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,7 +216,6 @@ ENDIF(WIN32) ENDIF(OGRE_STATIC) include_directories("." ${OGRE_INCLUDE_DIR} ${OGRE_INCLUDE_DIR}/Ogre ${OGRE_INCLUDE_DIR}/OGRE ${OGRE_PLUGIN_INCLUDE_DIRS} - ${OGRE_Terrain_INCLUDE_DIR} ${SDL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${PLATFORM_INCLUDE_DIR} diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 8259ac8cf..a44fd4b34 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -105,7 +105,6 @@ add_definitions(${SOUND_DEFINE}) target_link_libraries(openmw ${OGRE_LIBRARIES} - ${OGRE_Terrain_LIBRARY} ${OGRE_STATIC_PLUGINS} ${Boost_LIBRARIES} ${OPENAL_LIBRARY} From 7f0f9037be06db17016813dc4ff52962d73937c4 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Mon, 19 Aug 2013 22:57:21 +0100 Subject: [PATCH 056/194] fix for older versions of unshield --- apps/launcher/unshieldthread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index 5a9475d96..ab9d984e1 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -325,7 +325,7 @@ bool UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, cons void UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) { Unshield * unshield; - unshield = unshield_open_force_version(cab.c_str(), -1); + unshield = unshield_open(cab.c_str()); int i; for (i = 0; i < unshield_file_group_count(unshield); i++) From 8c8653160d90cb5b37cb164861f3f2ae4f26ba2f Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 20 Aug 2013 09:52:27 +0200 Subject: [PATCH 057/194] Crash fix, material fix --- apps/openmw/mwrender/renderingmanager.cpp | 37 ++++++++++++++++------- apps/openmw/mwrender/renderingmanager.hpp | 2 ++ apps/openmw/mwworld/scene.cpp | 4 +++ components/terrain/material.cpp | 28 +++++++++++------ components/terrain/material.hpp | 5 +++ components/terrain/quadtreenode.cpp | 37 +++++++++++++++++++++-- components/terrain/quadtreenode.hpp | 5 +++ components/terrain/storage.cpp | 2 +- components/terrain/terrain.cpp | 18 +++++++++++ components/terrain/terrain.hpp | 9 +++++- files/materials/terrain.shader | 1 - 11 files changed, 123 insertions(+), 25 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 63de044c8..2d129a3fb 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -244,16 +244,6 @@ void RenderingManager::cellAdded (MWWorld::Ptr::CellStore *store) mObjects.buildStaticGeometry (*store); sh::Factory::getInstance().unloadUnreferencedMaterials(); mDebugging->cellAdded(store); - if (store->isExterior()) - { - if (!mTerrain) - { - mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, - Settings::Manager::getBool("distant land", "Terrain"), - Settings::Manager::getBool("shader", "Terrain")); - mTerrain->update(mRendering.getCamera()->getRealPosition()); - } - } waterAdded(store); } @@ -848,7 +838,12 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec mWater->processChangedSettings(settings); if (rebuild) + { mObjects.rebuildStaticGeometry(); + if (mTerrain) + mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), + Settings::Manager::getBool("split", "Shadows")); + } } void RenderingManager::setMenuTransparency(float val) @@ -994,7 +989,7 @@ void RenderingManager::updateWaterRippleEmitterPtr (const MWWorld::Ptr& old, con void RenderingManager::frameStarted(float dt, bool paused) { - if (mTerrain) + if (mTerrain && mTerrain->getVisible()) mTerrain->update(mRendering.getCamera()->getRealPosition()); if (!paused) @@ -1012,4 +1007,24 @@ float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) return mTerrain->getHeightAt(worldPos); } +void RenderingManager::enableTerrain(bool enable) +{ + if (enable) + { + if (!mTerrain) + { + mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + Settings::Manager::getBool("distant land", "Terrain"), + Settings::Manager::getBool("shader", "Terrain")); + mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), + Settings::Manager::getBool("split", "Shadows")); + mTerrain->update(mRendering.getCamera()->getRealPosition()); + } + mTerrain->setVisible(true); + } + else + if (mTerrain) + mTerrain->setVisible(false); +} + } // namespace diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index ea6e8d409..45929f064 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -110,6 +110,8 @@ public: void cellAdded (MWWorld::CellStore *store); void waterAdded(MWWorld::CellStore *store); + void enableTerrain(bool enable); + void removeWater(); void preCellChange (MWWorld::CellStore* store); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 07155d349..9e96eebf1 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -362,6 +362,8 @@ namespace MWWorld const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + mRendering.enableTerrain(false); + std::string loadingInteriorText; loadingInteriorText = gmst.find ("sLoadingMessage2")->getString(); @@ -440,6 +442,8 @@ namespace MWWorld MWBase::Environment::get().getWorld()->positionToIndex (position.pos[0], position.pos[1], x, y); + mRendering.enableTerrain(true); + changeCell (x, y, position, true); } diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index f85326492..4a91ad99a 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -34,6 +34,8 @@ namespace Terrain MaterialGenerator::MaterialGenerator(bool shaders) : mShaders(shaders) + , mShadows(false) + , mSplitShadows(false) { } @@ -46,6 +48,11 @@ namespace Terrain // first layer doesn't need blendmap --freeTextureUnits; + if (mSplitShadows) + freeTextureUnits -= 3; + else if (mShadows) + --freeTextureUnits; + // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) return static_cast(freeTextureUnits / (1.25f)) + 1; } @@ -158,7 +165,6 @@ namespace Terrain } else { - sh::MaterialInstance* material = sh::Factory::getInstance().createMaterialInstance (name.str()); material->setProperty ("allow_fixed_function", sh::makeProperty(new sh::BooleanValue(false))); @@ -179,12 +185,14 @@ namespace Terrain tex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); // shadow. TODO: repeated, put in function - for (Ogre::uint i = 0; i < 3; ++i) + if (mShadows) { - sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); - shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + for (Ogre::uint i = 0; i < (mSplitShadows ? 3 : 1); ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } } - p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( Ogre::StringConverter::toString(1)))); @@ -272,12 +280,14 @@ namespace Terrain } // shadow - for (Ogre::uint i = 0; i < 3; ++i) + if (mShadows) { - sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); - shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + for (Ogre::uint i = 0; i < (mSplitShadows ? 3 : 1); ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } } - p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass)))); diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index 068bb7a84..2788b79c4 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -21,6 +21,9 @@ namespace Terrain const std::vector& getBlendmapList() { return mBlendmapList; } void setCompositeMap (const std::string& name) { mCompositeMap = name; } + void enableShadows(bool shadows) { mShadows = shadows; } + void enableSplitShadows(bool splitShadows) { mSplitShadows = splitShadows; } + /// Creates a material suitable for displaying a chunk of terrain using alpha-blending. /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case /// a new material is created. @@ -48,6 +51,8 @@ namespace Terrain std::vector mBlendmapList; std::string mCompositeMap; bool mShaders; + bool mShadows; + bool mSplitShadows; }; } diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 007977b0c..ec6a670dc 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -256,6 +256,10 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) mChunk->setVisibilityFlags(mTerrain->getVisiblityFlags()); mChunk->setCastShadows(true); mSceneNode->attachObject(mChunk); + + mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled()); + mMaterialGenerator->enableSplitShadows(mTerrain->getSplitShadowsEnabled()); + if (mSize == 1) { ensureLayerInfo(); @@ -338,8 +342,11 @@ void QuadTreeNode::destroyChunks() mMaterialGenerator->setCompositeMap(""); } - Ogre::TextureManager::getSingleton().remove(mCompositeMap->getName()); - mCompositeMap.setNull(); + if (!mCompositeMap.isNull()) + { + Ogre::TextureManager::getSingleton().remove(mCompositeMap->getName()); + mCompositeMap.setNull(); + } } else if (hasChildren()) for (int i=0; i<4; ++i) @@ -444,3 +451,29 @@ void QuadTreeNode::ensureCompositeMap() mTerrain->clearCompositeMapSceneManager(); } + +void QuadTreeNode::applyMaterials() +{ + if (mChunk) + { + mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled()); + mMaterialGenerator->enableSplitShadows(mTerrain->getSplitShadowsEnabled()); + if (mSize <= 1) + mChunk->setMaterial(mMaterialGenerator->generate(Ogre::MaterialPtr())); + else + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(Ogre::MaterialPtr())); + } + if (hasChildren()) + for (int i=0; i<4; ++i) + mChildren[i]->applyMaterials(); +} + +void QuadTreeNode::setVisible(bool visible) +{ + if (!visible && mChunk) + mChunk->setVisible(false); + + if (hasChildren()) + for (int i=0; i<4; ++i) + mChildren[i]->setVisible(visible); +} diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index 1b7c71b5b..bccd26642 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -49,6 +49,11 @@ namespace Terrain QuadTreeNode (Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); ~QuadTreeNode(); + void setVisible(bool visible); + + /// Rebuild all materials + void applyMaterials(); + /// Initialize neighbours - do this after the quadtree is built void initNeighbours(); /// Initialize bounding boxes of non-leafs by merging children bounding boxes. diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp index 800af3282..900e536ec 100644 --- a/components/terrain/storage.cpp +++ b/components/terrain/storage.cpp @@ -425,7 +425,7 @@ namespace Terrain 0---1 0---1 */ - // Build all 4 positions in terrain space, using point-sampled height + // Build all 4 positions in normalized cell space, using point-sampled height Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp index d06394aca..3d65fbfc1 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/terrain.cpp @@ -370,5 +370,23 @@ namespace Terrain return mStorage->getHeightAt(worldPos); } + void Terrain::applyMaterials(bool shadows, bool splitShadows) + { + mShadows = shadows; + mSplitShadows = splitShadows; + mRootNode->applyMaterials(); + } + + void Terrain::setVisible(bool visible) + { + mVisible = visible; + mRootNode->setVisible(visible); + } + + bool Terrain::getVisible() + { + return mVisible; + } + } diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp index 4844e0056..37ea2742a 100644 --- a/components/terrain/terrain.hpp +++ b/components/terrain/terrain.hpp @@ -40,6 +40,8 @@ namespace Terrain bool getDistantLandEnabled() { return mDistantLand; } bool getShadersEnabled() { return mShaders; } + bool getShadowsEnabled() { return mShadows; } + bool getSplitShadowsEnabled() { return mSplitShadows; } float getHeightAt (const Ogre::Vector3& worldPos); @@ -56,14 +58,16 @@ namespace Terrain Storage* getStorage() { return mStorage; } /// Show or hide the whole terrain + /// @note this setting will be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden void setVisible(bool visible); + bool getVisible(); /// Recreate materials used by terrain chunks. This should be called whenever settings of /// the material factory are changed. (Relying on the factory to update those materials is not /// enough, since turning a feature on/off can change the number of texture units available for layer/blend /// textures, and to properly respond to this we may need to change the structure of the material, such as /// adding or removing passes. This can only be achieved by a full rebuild.) - void applyMaterials(); + void applyMaterials(bool shadows, bool splitShadows); int getVisiblityFlags() { return mVisibilityFlags; } @@ -74,6 +78,9 @@ namespace Terrain private: bool mDistantLand; bool mShaders; + bool mShadows; + bool mSplitShadows; + bool mVisible; QuadTreeNode* mRootNode; Storage* mStorage; diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index dd016555f..80837a2cb 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -348,7 +348,6 @@ float2 blendUV = (UV - 0.5) * (16.0 / (16.0+1.0)) + 0.5; #else shOutputColour(0).a = 1.f-previousAlpha; #endif - } #endif From a17997a973306d3d652d20381c669f83428fe4ab Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 20 Aug 2013 02:10:18 -0700 Subject: [PATCH 058/194] Continually add bits of input velocity to inertia when falling --- apps/openmw/mwworld/physicssystem.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 48d397d31..a1d759ea8 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -142,13 +142,12 @@ namespace MWWorld else { velocity = Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z) * movement; - if(physicActor->getOnGround()) - inertia = velocity; - else + if(!physicActor->getOnGround()) { - inertia = physicActor->getInertialForce(); - velocity += inertia; + // If falling, add part of the incoming velocity with the current inertia + velocity = velocity*time + physicActor->getInertialForce(); } + inertia = velocity; if(!(movement.z > 0.0f)) { @@ -214,7 +213,7 @@ namespace MWWorld physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { - inertia.z -= time*627.2f; + inertia.z += time*-627.2f; physicActor->setInertialForce(inertia); } physicActor->setOnGround(isOnGround); From 0545622f5ad2749cd507fad326776dea54ad3645 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 20 Aug 2013 11:04:26 +0200 Subject: [PATCH 059/194] Fix werewolf state not getting completely reset when starting a new game --- apps/openmw/mwgui/windowmanagerimp.cpp | 3 +++ apps/openmw/mwworld/worldimp.cpp | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index c0a30206b..1cb1d80b0 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -248,6 +248,9 @@ namespace MWGui delete mCharGen; mCharGen = new CharacterCreation(); mGuiModes.clear(); + mHud->unsetSelectedWeapon(); + mHud->unsetSelectedSpell(); + unsetForceHide(GW_ALL); } else allow(GW_ALL); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index f0c55c43e..aceb9fe34 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -240,12 +240,13 @@ namespace MWWorld // Rebuild player setupPlayer(); MWWorld::Ptr player = mPlayer->getPlayer(); - renderPlayer(); - mRendering->resetCamera(); // removes NpcStats, ContainerStore etc player.getRefData().setCustomData(NULL); + renderPlayer(); + mRendering->resetCamera(); + // make sure to do this so that local scripts from items that were in the players inventory are removed mLocalScripts.clear(); From fa76d1631b35895d8b61034e789c714cd7abeb8c Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 20 Aug 2013 12:08:46 +0200 Subject: [PATCH 060/194] Some terrain fixes --- apps/openmw/mwrender/renderingmanager.cpp | 3 ++- apps/openmw/mwworld/worldimp.cpp | 9 +++------ components/terrain/quadtreenode.cpp | 4 ++-- components/terrain/storage.hpp | 2 ++ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 2d129a3fb..44f421a6e 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -1003,7 +1003,8 @@ void RenderingManager::resetCamera() float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) { - assert(mTerrain); + if (!mTerrain || !mTerrain->getVisible()) + return -std::numeric_limits::max(); return mTerrain->getHeightAt(worldPos); } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index aceb9fe34..b44ca3069 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1027,13 +1027,10 @@ namespace MWWorld return; } - if (ptr.getCell()->isExterior()) - { - float terrainHeight = mRendering->getTerrainHeightAt(pos); + float terrainHeight = mRendering->getTerrainHeightAt(pos); - if (pos.z < terrainHeight) - pos.z = terrainHeight; - } + if (pos.z < terrainHeight) + pos.z = terrainHeight; ptr.getRefData().getPosition().pos[2] = pos.z + 20; // place slightly above. will snap down to ground with code below diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index ec6a670dc..7faf81a5c 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -46,12 +46,12 @@ namespace // Algorithm described by Hanan Samet - 'Neighbour Finding in Quadtrees' // http://www.cs.umd.edu/~hjs/pubs/SametPRIP81.pdf - Terrain::QuadTreeNode* searchNeighbourRecursive (Terrain::QuadTreeNode* currentNode, Terrain::Direction dir) + QuadTreeNode* searchNeighbourRecursive (QuadTreeNode* currentNode, Direction dir) { if (!currentNode->getParent()) return NULL; // Arrived at root node, the root node does not have neighbours - Terrain::QuadTreeNode* nextNode; + QuadTreeNode* nextNode; if (adjacent(currentNode->getDirection(), dir)) nextNode = searchNeighbourRecursive(currentNode->getParent(), dir); else diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index 419439e19..b82f6bbb6 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -14,6 +14,8 @@ namespace Terrain /// We keep storage of terrain data abstract here since we need different implementations for game and editor class Storage { + public: + virtual ~Storage() {} private: virtual ESM::Land* getLand (int cellX, int cellY) = 0; virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0; From ecf7786d11eacb58a1b7029f29f4dbbbed6dfde0 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 20 Aug 2013 12:31:47 +0200 Subject: [PATCH 061/194] terrain_num_lights was removed --- apps/openmw/mwrender/renderingmanager.cpp | 1 - files/materials/terrain.shader | 7 ------- 2 files changed, 8 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 44f421a6e..1446f8aaa 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -145,7 +145,6 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b sh::Factory::getInstance ().setGlobalSetting ("fog", "true"); sh::Factory::getInstance ().setGlobalSetting ("num_lights", Settings::Manager::getString ("num lights", "Objects")); - sh::Factory::getInstance ().setGlobalSetting ("terrain_num_lights", Settings::Manager::getString ("num lights", "Terrain")); sh::Factory::getInstance ().setGlobalSetting ("simple_water", Settings::Manager::getBool("shader", "Water") ? "false" : "true"); sh::Factory::getInstance ().setGlobalSetting ("render_refraction", "false"); diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index 80837a2cb..2bff6d58f 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -200,13 +200,6 @@ @shPassthroughFragmentInputs - shUniform(float4, lightAmbient) @shAutoConstant(lightAmbient, ambient_light_colour) - @shForeach(@shGlobalSettingString(terrain_num_lights)) - shUniform(float4, lightPosObjSpace@shIterator) @shAutoConstant(lightPosObjSpace@shIterator, light_position, @shIterator) - shUniform(float4, lightAttenuation@shIterator) @shAutoConstant(lightAttenuation@shIterator, light_attenuation, @shIterator) - shUniform(float4, lightDiffuse@shIterator) @shAutoConstant(lightDiffuse@shIterator, light_diffuse_colour, @shIterator) - @shEndForeach - #if SHADOWS shSampler2D(shadowMap0) shUniform(float2, invShadowmapSize0) @shAutoConstant(invShadowmapSize0, inverse_texture_size, @shPropertyString(shadowtexture_offset)) From d9040df9d697733a628d00dbe5cb617603ad0ce2 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 20 Aug 2013 04:41:52 -0700 Subject: [PATCH 062/194] Increase the max number of physics iterations Should help with highly tessellated collision meshes with high framerates. --- apps/openmw/mwworld/physicssystem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index a1d759ea8..558d3f037 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -31,7 +31,7 @@ namespace MWWorld static const float sMaxSlope = 60.0f; static const float sStepSize = 30.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. - static const int sMaxIterations = 4; + static const int sMaxIterations = 8; class MovementSolver { From a61215dab1c6b486feec67d420a8fe199287e8aa Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 20 Aug 2013 16:04:06 +0200 Subject: [PATCH 063/194] some general filter parser cleanup --- apps/opencs/model/filter/parser.cpp | 226 ++++++++++++++++++++++--- apps/opencs/model/filter/parser.hpp | 34 ++-- apps/opencs/view/filter/editwidget.cpp | 4 +- 3 files changed, 220 insertions(+), 44 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 06a974bbe..305bf7a7d 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -1,7 +1,11 @@ #include "parser.hpp" +#include #include +#include + +#include #include "booleannode.hpp" @@ -12,65 +16,239 @@ namespace CSMFilter enum Type { Type_EOS, - Type_None + Type_None, + Type_String, + Type_Number, + Type_Open, + Type_Close, + Type_OpenSquare, + Type_CloseSquare, + Type_Comma, + Type_Keyword_True, ///< \attention Keyword enums must be arranged continuously. + Type_Keyword_False, + Type_Keyword_And, + Type_Keyword_Or, + Type_Keyword_Not }; Type mType; + std::string mString; + double mNumber; Token (Type type); + + Token (const std::string& string); + + Token (double number); + + operator bool() const; }; Token::Token (Type type) : mType (type) {} + + Token::Token (const std::string& string) : mType (Type_String), mString (string) {} + + Token::Token (double number) : mType (Type_Number), mNumber (number) {} + + Token::operator bool() const + { + return mType!=Type_None; + } + + bool operator== (const Token& left, const Token& right) + { + if (left.mType!=right.mType) + return false; + + switch (left.mType) + { + case Token::Type_String: return left.mString==right.mString; + case Token::Type_Number: return left.mNumber==right.mNumber; + + default: return true; + } + } } -CSMFilter::Token CSMFilter::Parser::getNextToken (const std::string& filter, int& index) const +CSMFilter::Token CSMFilter::Parser::getStringToken() { - if (index>=static_cast (filter.size())) + std::string string; + + int size = static_cast (mInput.size()); + + for (; mIndex1) + { + ++mIndex; + break; + } + }; + + if (!string.empty()) + { + if (string[0]=='"' && (string[string.size()-1]!='"' || string.size()<2) ) + { + error(); + return Token (Token::Type_None); + } + + if (string[0]!='"' && string[string.size()-1]=='"') + { + error(); + return Token (Token::Type_None); + } + } + + return checkKeywords (string); +} + +CSMFilter::Token CSMFilter::Parser::getNumberToken() +{ + std::string string; + + int size = static_cast (mInput.size()); + + bool hasDecimalPoint = false; + bool hasDigit = false; + + for (; mIndex> value; + + return value; +} + +CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) +{ + static const char *sKeywords[] = + { + "true", "false", + "and", "or", "not", + 0 + }; + + std::string string = Misc::StringUtils::lowerCase (token.mString); + + for (int i=0; sKeywords[i]; ++i) + if (sKeywords[i]==string) + return Token (static_cast (i+Token::Type_Keyword_True)); + + return token; +} + +CSMFilter::Token CSMFilter::Parser::getNextToken() +{ + int size = static_cast (mInput.size()); + + char c = 0; + + for (; mIndex=size) return Token (Token::Type_EOS); + switch (c) + { + case '(': ++mIndex; return Token (Token::Type_Open); + case ')': ++mIndex; return Token (Token::Type_Close); + case '[': ++mIndex; return Token (Token::Type_OpenSquare); + case ']': ++mIndex; return Token (Token::Type_CloseSquare); + case ',': ++mIndex; return Token (Token::Type_Comma); + } + + if (c=='"' || c=='_' || std::isalpha (c)) + return getStringToken(); + + if (c=='-' || c=='.' || std::isdigit (c)) + return getNumberToken(); + + error(); return Token (Token::Type_None); } -bool CSMFilter::Parser::isEndState() const +boost::shared_ptr CSMFilter::Parser::parseImp() { - return mState==State_End || mState==State_UnexpectedCharacter; + + + + return boost::shared_ptr(); } -CSMFilter::Parser::Parser() : mState (State_Begin) {} +void CSMFilter::Parser::error() +{ + mError = true; +} -void CSMFilter::Parser::parse (const std::string& filter) +CSMFilter::Parser::Parser() : mIndex (0), mError (false) {} + +bool CSMFilter::Parser::parse (const std::string& filter) { // reset - mState = State_Begin; mFilter.reset(); - int index = 0; + mError = false; + mInput = filter; + mIndex = 0; - while (!isEndState()) - { - Token token = getNextToken (filter, index); + boost::shared_ptr node = parseImp(); - switch (token.mType) - { - case Token::Type_None: mState = State_UnexpectedCharacter; break; - case Token::Type_EOS: mState = State_End; break; - } - } + if (mError) + return false; - if (mState==State_End && !mFilter) + if (node) + mFilter = node; + else { // Empty filter string equals to filter "true". mFilter.reset (new BooleanNode (true)); } -} -CSMFilter::Parser::State CSMFilter::Parser::getState() const -{ - return mState; + return true; } boost::shared_ptr CSMFilter::Parser::getFilter() const { - if (mState!=State_End) + if (mError) throw std::logic_error ("No filter available"); return mFilter; diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 2d616b9e1..4341fae9f 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -11,36 +11,36 @@ namespace CSMFilter class Parser { - public: + boost::shared_ptr mFilter; + std::string mInput; + int mIndex; + bool mError; - enum State - { - State_Begin, - State_UnexpectedCharacter, - State_End - }; + Token getStringToken(); - private: + Token getNumberToken(); - State mState; - boost::shared_ptr mFilter; + Token getNextToken(); - Token getNextToken (const std::string& filter, int& index) const; + Token checkKeywords (const Token& token); + ///< Turn string token into keyword token, if possible. - bool isEndState() const; - ///< This includes error states. + boost::shared_ptr parseImp(); + ///< Will return a null-pointer, if there is nothing more to parse. + + void error(); public: Parser(); - void parse (const std::string& filter); + bool parse (const std::string& filter); ///< Discards any previous calls to parse - - State getState() const; + /// + /// \return Success? boost::shared_ptr getFilter() const; - ///< Throws an exception if getState()!=State_End + ///< Throws an exception if the last call to parse did not return true. }; } diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 79cbc1dce..793e0504e 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -10,9 +10,7 @@ CSVFilter::EditWidget::EditWidget (QWidget *parent) void CSVFilter::EditWidget::textChanged (const QString& text) { - mParser.parse (text.toUtf8().constData()); - - if (mParser.getState()==CSMFilter::Parser::State_End) + if (mParser.parse (text.toUtf8().constData())) { setPalette (mPalette); emit filterChanged (mParser.getFilter(), ""); From 1a9672e31bcc29642690eccafe0f79e1aaf6d83c Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 20 Aug 2013 11:31:49 -0700 Subject: [PATCH 064/194] Don't update player physics more than 60 times a second Bullet and/or our collision handling code doesn't like timesteps much smaller than that. Ideally we should do physics in 60fps (or even 30fps) steps and use prediction and interpolation to get more, but that's not straight forward and needs a fixed timestep loop to lock physics and logic together. --- apps/openmw/mwworld/physicssystem.cpp | 29 ++++++++++++++------------- apps/openmw/mwworld/physicssystem.hpp | 5 +++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 558d3f037..c865629ac 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -225,7 +225,7 @@ namespace MWWorld PhysicsSystem::PhysicsSystem(OEngine::Render::OgreRenderer &_rend) : - mRender(_rend), mEngine(0) + mRender(_rend), mEngine(0), mTimeAccum(0.0f) { // Create physics. shapeLoader is deleted by the physic engine NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); @@ -394,11 +394,6 @@ namespace MWWorld return mEngine->getCollisions(ptr.getRefData().getBaseNode()->getName()); } - Ogre::Vector3 PhysicsSystem::move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, bool gravity) - { - return MovementSolver::move(ptr, movement, time, gravity, mEngine); - } - Ogre::Vector3 PhysicsSystem::traceDown(const MWWorld::Ptr &ptr) { return MovementSolver::traceDown(ptr, mEngine); @@ -570,16 +565,22 @@ namespace MWWorld { mMovementResults.clear(); - const MWBase::World *world = MWBase::Environment::get().getWorld(); - PtrVelocityList::iterator iter = mMovementQueue.begin(); - for(;iter != mMovementQueue.end();iter++) + mTimeAccum += dt; + if(mTimeAccum >= 1.0f/60.0f) { - Ogre::Vector3 newpos; - newpos = move(iter->first, iter->second, dt, - !world->isSwimming(iter->first) && !world->isFlying(iter->first)); - mMovementResults.push_back(std::make_pair(iter->first, newpos)); - } + const MWBase::World *world = MWBase::Environment::get().getWorld(); + PtrVelocityList::iterator iter = mMovementQueue.begin(); + for(;iter != mMovementQueue.end();iter++) + { + Ogre::Vector3 newpos; + newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, + !world->isSwimming(iter->first) && + !world->isFlying(iter->first), mEngine); + mMovementResults.push_back(std::make_pair(iter->first, newpos)); + } + mTimeAccum = 0.0f; + } mMovementQueue.clear(); return mMovementResults; diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 438e2ada6..bbee2d5da 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -52,8 +52,7 @@ namespace MWWorld void scaleObject (const MWWorld::Ptr& ptr); bool toggleCollisionMode(); - - Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, bool gravity); + std::vector getCollisions(const MWWorld::Ptr &ptr); ///< get handles this object collides with Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr); @@ -98,6 +97,8 @@ namespace MWWorld PtrVelocityList mMovementQueue; PtrVelocityList mMovementResults; + float mTimeAccum; + PhysicsSystem (const PhysicsSystem&); PhysicsSystem& operator= (const PhysicsSystem&); }; From 3bf3bd4b8c81b5a7a542f6d4d7b4da1a92e2546a Mon Sep 17 00:00:00 2001 From: vorenon Date: Wed, 21 Aug 2013 13:53:49 +0200 Subject: [PATCH 065/194] Silenced some warnings --- apps/openmw/mwgui/quickkeysmenu.cpp | 1 - apps/openmw/mwgui/travelwindow.cpp | 2 -- apps/openmw/mwscript/aiextensions.cpp | 1 - apps/openmw/mwscript/guiextensions.cpp | 5 ----- apps/openmw/mwscript/miscextensions.cpp | 2 -- 5 files changed, 11 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index a5fc2ca5d..5f749d3d3 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -605,7 +605,6 @@ namespace MWGui void MagicSelectionDialog::onEnchantedItemSelected(MyGUI::Widget* _sender) { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayer().getPlayer(); MWWorld::Ptr item = *_sender->getUserData(); mParent->onAssignMagicItem (item); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 83e158e4a..93ac8299d 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -100,8 +100,6 @@ namespace MWGui mPtr = actor; clearDestinations(); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayer().getPlayer(); - for(unsigned int i = 0;i()->mBase->mTransport.size();i++) { std::string cellname = mPtr.get()->mBase->mTransport[i].mCellName; diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index b77d61aa5..fac44c08f 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -352,7 +352,6 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { - MWWorld::Ptr ptr = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index 4bb10dad5..6c89a0d1c 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -67,11 +67,6 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime) { - InterpreterContext& context = - static_cast (runtime.getContext()); - - MWWorld::Ptr ptr = context.getReference(); - runtime.push (MWBase::Environment::get().getWindowManager()->readPressedButton()); } }; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 77dc1deb0..3141f13a2 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -102,8 +102,6 @@ namespace MWScript InterpreterContext& context = static_cast (runtime.getContext()); - MWWorld::Ptr ptr = context.getReference(); - context.executeActivation(); } }; From a546ace94d57b6991d3c6ca9fe0d4964a9ff751b Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 21 Aug 2013 00:38:39 -0700 Subject: [PATCH 066/194] Remove an unused method --- apps/openmw/mwworld/physicssystem.cpp | 5 ----- apps/openmw/mwworld/physicssystem.hpp | 2 -- apps/openmw/mwworld/scene.cpp | 3 --- 3 files changed, 10 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index c865629ac..826f0ed9a 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -318,11 +318,6 @@ namespace MWWorld } - void PhysicsSystem::setCurrentWater(bool hasWater, int waterHeight) - { - // TODO: store and use - } - btVector3 PhysicsSystem::getRayPoint(float extent) { //get a ray pointing to the center of the viewport diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index bbee2d5da..669eb74bd 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -78,8 +78,6 @@ namespace MWWorld OEngine::Physic::PhysicEngine* getEngine(); - void setCurrentWater(bool hasWater, int waterHeight); - bool getObjectAABB(const MWWorld::Ptr &ptr, Ogre::Vector3 &min, Ogre::Vector3 &max); /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 9e96eebf1..ee8973ae5 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -162,9 +162,6 @@ namespace MWWorld void Scene::playerCellChange(MWWorld::CellStore *cell, const ESM::Position& pos, bool adjustPlayerPos) { - bool hasWater = cell->mCell->mData.mFlags & ESM::Cell::HasWater; - mPhysics->setCurrentWater(hasWater, cell->mCell->mWater); - MWBase::World *world = MWBase::Environment::get().getWorld(); world->getPlayer().setCell(cell); From 9f09bb6f6fbfe33ef2b4a3b8b11c84ed8b830836 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 21 Aug 2013 00:55:26 -0700 Subject: [PATCH 067/194] Use separate inputs for swimming and flying --- apps/openmw/mwworld/physicssystem.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 826f0ed9a..70e8e88ec 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -107,7 +107,7 @@ namespace MWWorld } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool gravity, OEngine::Physic::PhysicEngine *engine) + bool isSwimming, bool isFlying, OEngine::Physic::PhysicEngine *engine) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); @@ -132,7 +132,7 @@ namespace MWWorld bool isOnGround = false; Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; - if(!gravity) + if(isSwimming || isFlying) { velocity = (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* @@ -181,7 +181,7 @@ namespace MWWorld // We hit something. Try to step up onto it. if(stepMove(colobj, newPosition, velocity, remainingTime, engine)) - isOnGround = gravity; // Only on the ground if there's gravity + isOnGround = !(isSwimming || isFlying); // Only on the ground if there's gravity else { // Can't move this way, try to find another spot along the plane @@ -192,7 +192,7 @@ namespace MWWorld // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. - if(gravity) + if(!(isSwimming || isFlying)) velocity.z = std::min(velocity.z, 0.0f); } } @@ -209,7 +209,7 @@ namespace MWWorld isOnGround = false; } - if(isOnGround || !gravity) + if(isOnGround || isSwimming || isFlying) physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { @@ -569,8 +569,8 @@ namespace MWWorld { Ogre::Vector3 newpos; newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, - !world->isSwimming(iter->first) && - !world->isFlying(iter->first), mEngine); + world->isSwimming(iter->first), + world->isFlying(iter->first), mEngine); mMovementResults.push_back(std::make_pair(iter->first, newpos)); } From 602be9bbe7f26e042af38f36292a42133c5fb7e7 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 21 Aug 2013 07:06:07 -0700 Subject: [PATCH 068/194] Avoid swimming into the air from underwater --- apps/openmw/mwworld/physicssystem.cpp | 28 ++++++++++++++++++++++++--- components/esm/loadcell.hpp | 5 +++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 70e8e88ec..33ba7101e 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -107,7 +107,8 @@ namespace MWWorld } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool isSwimming, bool isFlying, OEngine::Physic::PhysicEngine *engine) + bool isSwimming, bool isFlying, float waterlevel, + OEngine::Physic::PhysicEngine *engine) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); @@ -127,6 +128,8 @@ namespace MWWorld Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); position.z += halfExtents.z; + waterlevel -= halfExtents.z * 0.5; + OEngine::Physic::ActorTracer tracer; bool wasOnGround = false; bool isOnGround = false; @@ -168,8 +171,21 @@ namespace MWWorld float remainingTime = time; for(int iterations = 0;iterations < sMaxIterations && remainingTime > 0.01f;++iterations) { + Ogre::Vector3 nextpos = newPosition + velocity*remainingTime; + + if(isSwimming && !isFlying && + nextpos.z > waterlevel && newPosition.z <= waterlevel) + { + const Ogre::Vector3 down(0,0,-1); + Ogre::Real movelen = velocity.normalise(); + Ogre::Vector3 reflectdir = velocity.reflect(down); + reflectdir.normalise(); + velocity = slide(reflectdir, down)*movelen; + continue; + } + // trace to where character would go if there were no obstructions - tracer.doTrace(colobj, newPosition, newPosition+velocity*remainingTime, engine); + tracer.doTrace(colobj, newPosition, nextpos, engine); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -567,10 +583,16 @@ namespace MWWorld PtrVelocityList::iterator iter = mMovementQueue.begin(); for(;iter != mMovementQueue.end();iter++) { + float waterlevel = -std::numeric_limits::max(); + const MWWorld::CellStore *cellstore = iter->first.getCell(); + if(cellstore->mCell->hasWater()) + waterlevel = cellstore->mCell->mWater; + Ogre::Vector3 newpos; newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, world->isSwimming(iter->first), - world->isFlying(iter->first), mEngine); + world->isFlying(iter->first), + waterlevel, mEngine); mMovementResults.push_back(std::make_pair(iter->first, newpos)); } diff --git a/components/esm/loadcell.hpp b/components/esm/loadcell.hpp index eda8a5418..51288b291 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm/loadcell.hpp @@ -119,6 +119,11 @@ struct Cell return mData.mY; } + bool hasWater() const + { + return (mData.mFlags&HasWater); + } + // Restore the given reader to the stored position. Will try to open // the file matching the stored file name. If you want to read from // somewhere other than the file system, you need to pre-open the From c855ab65cfbeeaf0ecbb798bf28f29c335af8471 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 21 Aug 2013 07:24:02 -0700 Subject: [PATCH 069/194] Non-colliding objects are considered flying --- apps/openmw/mwworld/worldimp.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 474453c72..950f00565 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1560,9 +1560,19 @@ namespace MWWorld bool World::isFlying(const MWWorld::Ptr &ptr) const { - const MWWorld::Class &cls = MWWorld::Class::get(ptr); - if(cls.isActor() && cls.getCreatureStats(ptr).getMagicEffects().get(MWMechanics::EffectKey(ESM::MagicEffect::Levitate)).mMagnitude > 0) + if(!ptr.getClass().isActor()) + return false; + + const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); + if(stats.getMagicEffects().get(MWMechanics::EffectKey(ESM::MagicEffect::Levitate)).mMagnitude > 0) + return true; + + // TODO: Check if flying creature + + const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(ptr.getRefData().getHandle()); + if(!actor || !actor->getCollisionMode()) return true; + return false; } From ce5ea6d7d20930d7425deb25038c83f2ba42a751 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 20 Aug 2013 16:42:24 +0200 Subject: [PATCH 070/194] Use a proper node hierarchy; disconnect the root when entering interior --- apps/openmw/mwrender/renderingmanager.cpp | 2 +- components/terrain/quadtreenode.cpp | 75 +++++++++++++++-------- components/terrain/quadtreenode.hpp | 13 ++-- components/terrain/terrain.cpp | 11 +++- components/terrain/terrain.hpp | 3 + 5 files changed, 71 insertions(+), 33 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 1446f8aaa..32ddd8b5f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -988,7 +988,7 @@ void RenderingManager::updateWaterRippleEmitterPtr (const MWWorld::Ptr& old, con void RenderingManager::frameStarted(float dt, bool paused) { - if (mTerrain && mTerrain->getVisible()) + if (mTerrain) mTerrain->update(mRendering.getCamera()->getRealPosition()); if (!paused) diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 7faf81a5c..ed6877f50 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -151,8 +151,15 @@ QuadTreeNode::QuadTreeNode(Terrain* terrain, ChildDirection dir, float size, con for (int i=0; i<4; ++i) mNeighbours[i] = NULL; - mSceneNode = mTerrain->getSceneManager()->getRootSceneNode()->createChildSceneNode( - Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + if (mDirection == Root) + mSceneNode = mTerrain->getRootSceneNode(); + else + mSceneNode = mTerrain->getSceneManager()->createSceneNode(); + Ogre::Vector2 pos (0,0); + if (mParent) + pos = mParent->getCenter(); + pos = mCenter - pos; + mSceneNode->setPosition(Ogre::Vector3(pos.x*8192, pos.y*8192, 0)); mLodLevel = log2(mSize); @@ -221,13 +228,12 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) float dist = distance(mWorldBounds, cameraPos); - if (!mTerrain->getDistantLandEnabled()) + bool distantLand = mTerrain->getDistantLandEnabled(); + + // Make sure our scene node is attached + if (!mSceneNode->isInSceneGraph()) { - if (dist > 8192*2) - { - destroyChunks(); - return; - } + mParent->getSceneNode()->addChild(mSceneNode); } /// \todo implement error metrics or some other means of not using arbitrary values @@ -246,9 +252,22 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (dist > 8192*64) wantedLod = 6; + bool hadChunk = hasChunk(); + + if (!distantLand && dist > 8192*2) + { + if (mIsActive) + { + destroyChunks(true); + mIsActive = false; + } + return; + } + + mIsActive = true; + if (mSize <= mTerrain->getMaxBatchSize() && mLodLevel <= wantedLod) { - bool hadChunk = hasChunk(); // Wanted LOD is small enough to render this node in one chunk if (!mChunk) { @@ -297,32 +316,36 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (!hadChunk && hasChildren()) { - for (int i=0; i<4; ++i) - mChildren[i]->hideChunks(); + // Make sure child scene nodes are detached + mSceneNode->removeAllChildren(); + + // If distant land is enabled, keep the chunks around in case we need them again, + // otherwise, prefer low memory usage + if (!distantLand) + for (int i=0; i<4; ++i) + mChildren[i]->destroyChunks(true); } } else { // Wanted LOD is too detailed to be rendered in one chunk, // so split it up by delegating to child nodes - if (mChunk) - mChunk->setVisible(false); + if (hadChunk) + { + // If distant land is enabled, keep the chunks around in case we need them again, + // otherwise, prefer low memory usage + if (!distantLand) + destroyChunks(false); + else if (mChunk) + mChunk->setVisible(false); + } assert(hasChildren() && "Leaf node's LOD needs to be 0"); for (int i=0; i<4; ++i) mChildren[i]->update(cameraPos); } } -void QuadTreeNode::hideChunks() -{ - if (mChunk) - mChunk->setVisible(false); - else if (hasChildren()) - for (int i=0; i<4; ++i) - mChildren[i]->hideChunks(); -} - -void QuadTreeNode::destroyChunks() +void QuadTreeNode::destroyChunks(bool children) { if (mChunk) { @@ -348,9 +371,9 @@ void QuadTreeNode::destroyChunks() mCompositeMap.setNull(); } } - else if (hasChildren()) + else if (children && hasChildren()) for (int i=0; i<4; ++i) - mChildren[i]->destroyChunks(); + mChildren[i]->destroyChunks(true); } void QuadTreeNode::updateIndexBuffers() @@ -366,7 +389,7 @@ void QuadTreeNode::updateIndexBuffers() bool QuadTreeNode::hasChunk() { - return mChunk && mChunk->getVisible(); + return mSceneNode->isInSceneGraph() && mChunk && mChunk->getVisible(); } size_t QuadTreeNode::getActualLodLevel() diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index bccd26642..fe646f3bf 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -72,6 +72,8 @@ namespace Terrain QuadTreeNode* getParent() { return mParent; } + Ogre::SceneNode* getSceneNode() { return mSceneNode; } + int getSize() { return mSize; } Ogre::Vector2 getCenter() { return mCenter; } @@ -100,11 +102,8 @@ namespace Terrain /// Call after QuadTreeNode::update! void updateIndexBuffers(); - /// Hide chunks rendered by this node and all its children - void hideChunks(); - - /// Destroy chunks rendered by this node and all its children - void destroyChunks(); + /// Destroy chunks rendered by this node *and* its children (if param is true) + void destroyChunks(bool children); /// Get the effective LOD level if this node was rendered in one chunk /// with ESM::Land::LAND_SIZE^2 vertices @@ -127,6 +126,10 @@ namespace Terrain // Stored here for convenience in case we need layer list again MaterialGenerator* mMaterialGenerator; + /// Is this node (or any of its child nodes) currently configured to render itself? + /// (only relevant when distant land is disabled, otherwise whole terrain is always rendered) + bool mIsActive; + bool mIsDummy; float mSize; size_t mLodLevel; // LOD if we were to render this node in one chunk diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp index 3d65fbfc1..da1ad1141 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/terrain.cpp @@ -59,6 +59,7 @@ namespace Terrain , mVisibilityFlags(visibilityFlags) , mDistantLand(distantLand) , mShaders(shaders) + , mVisible(true) { mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); @@ -81,6 +82,8 @@ namespace Terrain // Adjust the center according to the new size Ogre::Vector3 center = mBounds.getCenter() + Ogre::Vector3((size-origSizeX)/2.f, (size-origSizeY)/2.f, 0); + mRootSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(center.x, center.y), NULL); buildQuadTree(mRootNode); mRootNode->initAabb(); @@ -142,6 +145,8 @@ namespace Terrain void Terrain::update(const Ogre::Vector3& cameraPos) { + if (!mVisible) + return; mRootNode->update(cameraPos); mRootNode->updateIndexBuffers(); } @@ -379,8 +384,12 @@ namespace Terrain void Terrain::setVisible(bool visible) { + if (visible && !mVisible) + mSceneMgr->getRootSceneNode()->addChild(mRootSceneNode); + else if (!visible && mVisible) + mSceneMgr->getRootSceneNode()->removeChild(mRootSceneNode); + mVisible = visible; - mRootNode->setVisible(visible); } bool Terrain::getVisible() diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp index 37ea2742a..4d329f9e1 100644 --- a/components/terrain/terrain.hpp +++ b/components/terrain/terrain.hpp @@ -55,6 +55,8 @@ namespace Terrain Ogre::SceneManager* getSceneManager() { return mSceneMgr; } + Ogre::SceneNode* getRootSceneNode() { return mRootSceneNode; } + Storage* getStorage() { return mStorage; } /// Show or hide the whole terrain @@ -83,6 +85,7 @@ namespace Terrain bool mVisible; QuadTreeNode* mRootNode; + Ogre::SceneNode* mRootSceneNode; Storage* mStorage; int mVisibilityFlags; From 43313437dc98d0732c8f44dbdb145f4dc6644673 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 21 Aug 2013 15:19:11 +0200 Subject: [PATCH 071/194] Fix composite map for cells without land data --- components/terrain/quadtreenode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index ed6877f50..ed5673877 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -423,7 +423,7 @@ void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) std::vector layer; layer.push_back("_land_default.dds"); matGen.setLayerList(layer); - makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generate(Ogre::MaterialPtr())); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generateForCompositeMapRTT(Ogre::MaterialPtr())); return; } if (mSize > 1) From 5f7e6f7b104da9ba53e0061db9970958fef53129 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 21 Aug 2013 16:01:32 +0200 Subject: [PATCH 072/194] Fix a material issue, layers per pass wasn't entirely correct --- components/terrain/material.cpp | 98 ++++++++++++++++----------------- components/terrain/material.hpp | 4 -- files/materials/terrain.shader | 4 +- 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 4a91ad99a..ebf6046ff 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -40,29 +40,6 @@ namespace Terrain } - int MaterialGenerator::getMaxLayersPerPass () - { - // count the texture units free - Ogre::uint8 freeTextureUnits = 16; - - // first layer doesn't need blendmap - --freeTextureUnits; - - if (mSplitShadows) - freeTextureUnits -= 3; - else if (mShadows) - --freeTextureUnits; - - // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) - return static_cast(freeTextureUnits / (1.25f)) + 1; - } - - int MaterialGenerator::getRequiredPasses () - { - int maxLayersPerPass = getMaxLayersPerPass(); - return std::max(1.f, std::ceil(static_cast(mLayerList.size()) / maxLayersPerPass)); - } - Ogre::MaterialPtr MaterialGenerator::generate(Ogre::MaterialPtr mat) { return create(mat, false, false); @@ -201,45 +178,64 @@ namespace Terrain else { - int numPasses = getRequiredPasses(); - assert(numPasses); - int maxLayersInOnePass = getMaxLayersPerPass(); + bool shadows = mShadows && !renderCompositeMap; - for (int pass=0; pass blendTextures; + int remainingTextureUnits = OGRE_MAX_TEXTURE_LAYERS; + if (shadows) + remainingTextureUnits -= (mSplitShadows ? 3 : 1); + while (remainingTextureUnits && layerOffset + numLayersInThisPass < (int)mLayerList.size()) + { + int layerIndex = numLayersInThisPass + layerOffset; + + int neededTextureUnits=0; + int neededBlendTextures=0; + + if (layerIndex != 0) + { + std::string blendTextureName = mBlendmapList[getBlendmapIndexForLayer(layerIndex)]->getName(); + if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) + { + blendTextures.push_back(blendTextureName); + ++neededBlendTextures; + ++neededTextureUnits; // blend texture + } + } + ++neededTextureUnits; // layer texture + if (neededTextureUnits <= remainingTextureUnits) + { + // We can fit another! + remainingTextureUnits -= neededTextureUnits; + numBlendTextures += neededBlendTextures; + ++numLayersInThisPass; + } + else + break; // We're full + } + sh::MaterialInstancePass* p = material->createPass (); p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); - if (pass != 0) + if (layerOffset != 0) { p->setProperty ("scene_blend", sh::makeProperty(new sh::StringValue("alpha_blend"))); // Only write if depth is equal to the depth value written by the previous pass. p->setProperty ("depth_func", sh::makeProperty(new sh::StringValue("equal"))); } - p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(pass == 0))); p->mShaderProperties.setProperty ("render_composite_map", sh::makeProperty(new sh::BooleanValue(renderCompositeMap))); p->mShaderProperties.setProperty ("display_composite_map", sh::makeProperty(new sh::BooleanValue(displayCompositeMap))); - Ogre::uint numLayersInThisPass = std::min(maxLayersInOnePass, (int)mLayerList.size()-layerOffset); - - // a blend map might be shared between two passes - Ogre::uint numBlendTextures=0; - std::vector blendTextures; - for (unsigned int layer=blendmapOffset; layergetName(); - if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) - { - blendTextures.push_back(blendTextureName); - ++numBlendTextures; - } - } - p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numLayersInThisPass)))); p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numBlendTextures)))); @@ -250,7 +246,7 @@ namespace Terrain blendmapStart = 0; else blendmapStart = getBlendmapIndexForLayer(layerOffset+blendmapOffset); - for (Ogre::uint i = 0; i < numBlendTextures; ++i) + for (int i = 0; i < numBlendTextures; ++i) { sh::MaterialInstanceTextureUnit* blendTex = p->createTextureUnit ("blendMap" + Ogre::StringConverter::toString(i)); blendTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(mBlendmapList[blendmapStart+i]->getName()))); @@ -258,7 +254,7 @@ namespace Terrain } // layer maps - for (Ogre::uint i = 0; i < numLayersInThisPass; ++i) + for (int i = 0; i < numLayersInThisPass; ++i) { sh::MaterialInstanceTextureUnit* diffuseTex = p->createTextureUnit ("diffuseMap" + Ogre::StringConverter::toString(i)); diffuseTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue("textures\\"+mLayerList[layerOffset+i]))); @@ -280,7 +276,7 @@ namespace Terrain } // shadow - if (mShadows) + if (shadows) { for (Ogre::uint i = 0; i < (mSplitShadows ? 3 : 1); ++i) { @@ -292,7 +288,9 @@ namespace Terrain Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass)))); // Make sure the pass index is fed to the permutation handler, because blendmap components may be different - p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(pass))); + p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(layerOffset))); + + layerOffset += numLayersInThisPass; } } } diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index 2788b79c4..330ed3d14 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -43,10 +43,6 @@ namespace Terrain private: Ogre::MaterialPtr create (Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap); - int getRequiredPasses (); - int getMaxLayersPerPass (); - - int mNumLayers; std::vector mLayerList; std::vector mBlendmapList; std::string mCompositeMap; diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index 2bff6d58f..861841a84 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -39,7 +39,6 @@ #if LIGHTING @shAllocatePassthrough(3, lightResult) @shAllocatePassthrough(3, directionalResult) -#endif #if SHADOWS @shAllocatePassthrough(4, lightSpacePos0) @@ -49,6 +48,7 @@ @shAllocatePassthrough(4, lightSpacePos@shIterator) @shEndForeach #endif +#endif #ifdef SH_VERTEX_SHADER @@ -200,6 +200,7 @@ @shPassthroughFragmentInputs +#if LIGHTING #if SHADOWS shSampler2D(shadowMap0) shUniform(float2, invShadowmapSize0) @shAutoConstant(invShadowmapSize0, inverse_texture_size, @shPropertyString(shadowtexture_offset)) @@ -215,6 +216,7 @@ #if SHADOWS || SHADOWS_PSSM shUniform(float4, shadowFar_fadeStart) @shSharedParameter(shadowFar_fadeStart) #endif +#endif #if (UNDERWATER) || (FOG) shUniform(float4x4, worldMatrix) @shAutoConstant(worldMatrix, world_matrix) From 758d989a036d613ae1e33206a999aa59ad568ab7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 21 Aug 2013 17:24:24 +0200 Subject: [PATCH 073/194] If multiple plugins have land data for the same cell, the last plugin should win --- apps/openmw/mwworld/store.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 2c10e3edc..2ee23dbd6 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -466,6 +466,18 @@ namespace MWWorld ESM::Land *ptr = new ESM::Land(); ptr->load(esm); + // Same area defined in multiple plugins? -> last plugin wins + // Can't use search() because we aren't sorted yet - is there any other way to speed this up? + for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + { + if ((*it)->mX == ptr->mX && (*it)->mY == ptr->mY) + { + delete *it; + mStatic.erase(it); + break; + } + } + mStatic.push_back(ptr); } From d086346b0747845c3243a61a9daa22afd7b41b03 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 21 Aug 2013 17:25:29 +0200 Subject: [PATCH 074/194] Fix loading of some cells in TR --- components/esm/loadcell.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index dbd1fed6f..d8d0c1291 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -153,6 +153,14 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref) // That should be it, I haven't seen any other fields yet. } + // NAM0 sometimes appears here, sometimes further on + ref.mNam0 = 0; + if (esm.isNextSub("NAM0")) + { + esm.getHT(ref.mNam0); + //esm.getHNOT(NAM0, "NAM0"); + } + esm.getHNT(ref.mRefnum, "FRMR"); ref.mRefID = esm.getHNString("NAME"); @@ -243,7 +251,6 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref) // Update: Well, maybe not completely useless. This might actually be // number_of_references + number_of_references_moved_here_Across_boundaries, // and could be helpful for collecting these weird moved references. - ref.mNam0 = 0; if (esm.isNextSub("NAM0")) { esm.getHT(ref.mNam0); From dc6e15f38ed3b31d8c7389344064bc0612c92089 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Wed, 21 Aug 2013 23:59:33 +0400 Subject: [PATCH 075/194] Replaced std::pow with shift since clang doesn't like int as a first argument (reports that call is ambiguous) --- components/terrain/chunk.cpp | 4 ++-- components/terrain/storage.cpp | 2 +- components/terrain/terrain.cpp | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp index 7a81358d6..87222af8b 100644 --- a/components/terrain/chunk.cpp +++ b/components/terrain/chunk.cpp @@ -20,7 +20,7 @@ namespace Terrain // Set the total number of vertices size_t numVertsOneSide = mNode->getSize() * (ESM::Land::LAND_SIZE-1); - numVertsOneSide /= std::pow(2, lodLevel); + numVertsOneSide /= 1 << lodLevel; numVertsOneSide += 1; assert((int)numVertsOneSide == ESM::Land::LAND_SIZE); mVertexData->vertexCount = numVertsOneSide * numVertsOneSide; @@ -92,7 +92,7 @@ namespace Terrain // Use 4 bits for each LOD delta if (lod > 0) { - assert (lod - ourLod < std::pow(2,4)); + assert (lod - ourLod < (1 << 4)); flags |= int(lod - ourLod) << (4*i); } } diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp index 900e536ec..f00677e97 100644 --- a/components/terrain/storage.cpp +++ b/components/terrain/storage.cpp @@ -131,7 +131,7 @@ namespace Terrain Ogre::HardwareVertexBufferSharedPtr colourBuffer) { // LOD level n means every 2^n-th vertex is kept - size_t increment = std::pow(2, lodLevel); + size_t increment = 1 << lodLevel; Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); assert(origin.x == (int) origin.x); diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp index da1ad1141..1a88d63e4 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/terrain.cpp @@ -214,7 +214,7 @@ namespace Terrain bool anyDeltas = (lodDeltas[North] || lodDeltas[South] || lodDeltas[West] || lodDeltas[East]); - size_t increment = std::pow(2, lodLevel); + size_t increment = 1 << lodLevel; assert((int)increment < ESM::Land::LAND_SIZE); std::vector indices; indices.reserve((ESM::Land::LAND_SIZE-1)*(ESM::Land::LAND_SIZE-1)*2*3 / increment); @@ -251,7 +251,7 @@ namespace Terrain // South size_t row = 0; - size_t outerStep = std::pow(2, lodDeltas[South] + lodLevel); + size_t outerStep = 1 << (lodDeltas[South] + lodLevel); for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep) { indices.push_back(ESM::Land::LAND_SIZE*col+row); @@ -275,7 +275,7 @@ namespace Terrain // North row = ESM::Land::LAND_SIZE-1; - outerStep = std::pow(2, lodDeltas[North] + lodLevel); + outerStep = 1 << (lodDeltas[North] + lodLevel); for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep) { indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row); @@ -299,7 +299,7 @@ namespace Terrain // West size_t col = 0; - outerStep = std::pow(2, lodDeltas[West] + lodLevel); + outerStep = 1 << (lodDeltas[West] + lodLevel); for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep) { indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep); @@ -323,7 +323,7 @@ namespace Terrain // East col = ESM::Land::LAND_SIZE-1; - outerStep = std::pow(2, lodDeltas[East] + lodLevel); + outerStep = 1 << (lodDeltas[East] + lodLevel); for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep) { indices.push_back(ESM::Land::LAND_SIZE*col+row); From 2e8b0cf5023e70c6ca26484582efa8aa14622a66 Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Wed, 21 Aug 2013 17:14:29 -0500 Subject: [PATCH 076/194] Remove Directory Paths in Qt Classes Includes Qt recommends that the path location not be used when including a class. Also, this is how other files include Qt classes in the OpenCS app. This change is for consistency only. --- apps/opencs/editor.cpp | 2 +- apps/opencs/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 082fa376c..e05ebcdeb 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -1,7 +1,7 @@ #include "editor.hpp" -#include +#include #include "model/doc/document.hpp" #include "model/world/data.hpp" diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index aa315804b..7f6f9302e 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include class Application : public QApplication From 50041fc2112f4bc12e17ffdcda2d36cd236ad61c Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 09:17:57 +0200 Subject: [PATCH 077/194] simplified filter system by taking out filter references and user values for now (these are post-1.0 features anyway) --- apps/opencs/model/filter/booleannode.cpp | 4 +--- apps/opencs/model/filter/booleannode.hpp | 4 +--- apps/opencs/model/filter/leafnode.cpp | 10 -------- apps/opencs/model/filter/leafnode.hpp | 6 ----- apps/opencs/model/filter/narynode.cpp | 24 ------------------- apps/opencs/model/filter/narynode.hpp | 5 ---- apps/opencs/model/filter/node.hpp | 10 +------- apps/opencs/model/filter/unarynode.cpp | 9 ------- apps/opencs/model/filter/unarynode.hpp | 5 ---- apps/opencs/model/world/idtableproxymodel.cpp | 8 ++----- apps/opencs/model/world/idtableproxymodel.hpp | 4 +--- apps/opencs/view/world/table.cpp | 5 ++-- apps/opencs/view/world/table.hpp | 3 +-- 13 files changed, 9 insertions(+), 88 deletions(-) diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp index 2f521ed06..267e06a64 100644 --- a/apps/opencs/model/filter/booleannode.cpp +++ b/apps/opencs/model/filter/booleannode.cpp @@ -4,9 +4,7 @@ CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} bool CSMFilter::BooleanNode::test (const CSMWorld::IdTable& table, int row, - const std::map& otherFilters, - const std::map& columns, - const std::string& userValue) const + const std::map& columns) const { return mTrue; } diff --git a/apps/opencs/model/filter/booleannode.hpp b/apps/opencs/model/filter/booleannode.hpp index 3578c7f3f..d19219e35 100644 --- a/apps/opencs/model/filter/booleannode.hpp +++ b/apps/opencs/model/filter/booleannode.hpp @@ -14,9 +14,7 @@ namespace CSMFilter BooleanNode (bool true_); virtual bool test (const CSMWorld::IdTable& table, int row, - const std::map& otherFilters, - const std::map& columns, - const std::string& userValue) const; + const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/leafnode.cpp b/apps/opencs/model/filter/leafnode.cpp index 5c6d4b042..a1330f9dc 100644 --- a/apps/opencs/model/filter/leafnode.cpp +++ b/apps/opencs/model/filter/leafnode.cpp @@ -1,11 +1,6 @@ #include "leafnode.hpp" -std::vector CSMFilter::LeafNode::getReferencedFilters() const -{ - return std::vector(); -} - std::vector CSMFilter::LeafNode::getReferencedColumns() const { return std::vector(); @@ -15,8 +10,3 @@ bool CSMFilter::LeafNode::isSimple() const { return true; } - -bool CSMFilter::LeafNode::hasUserValue() const -{ - return false; -} \ No newline at end of file diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp index 663083ff9..2b0163065 100644 --- a/apps/opencs/model/filter/leafnode.hpp +++ b/apps/opencs/model/filter/leafnode.hpp @@ -11,18 +11,12 @@ namespace CSMFilter { public: - virtual std::vector getReferencedFilters() const; - ///< Return a list of filters that are used by this node (and must be passed as - /// otherFilters when calling test). - virtual std::vector getReferencedColumns() const; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. virtual bool isSimple() const; ///< \return Can this filter be displayed in simple mode. - - virtual bool hasUserValue() const; }; } diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp index 0756f6707..cc131d8ac 100644 --- a/apps/opencs/model/filter/narynode.cpp +++ b/apps/opencs/model/filter/narynode.cpp @@ -15,21 +15,6 @@ const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const return *mNodes.at (index); } -std::vector CSMFilter::NAryNode::getReferencedFilters() const -{ - std::vector filters; - - for (std::vector >::const_iterator iter (mNodes.begin()); - iter!=mNodes.end(); ++iter) - { - std::vector filters2 = (*iter)->getReferencedFilters(); - - filters.insert (filters.end(), filters2.begin(), filters2.end()); - } - - return filters; -} - std::vector CSMFilter::NAryNode::getReferencedColumns() const { std::vector columns; @@ -50,12 +35,3 @@ bool CSMFilter::NAryNode::isSimple() const return false; } -bool CSMFilter::NAryNode::hasUserValue() const -{ - for (std::vector >::const_iterator iter (mNodes.begin()); - iter!=mNodes.end(); ++iter) - if ((*iter)->hasUserValue()) - return true; - - return false; -} diff --git a/apps/opencs/model/filter/narynode.hpp b/apps/opencs/model/filter/narynode.hpp index 65e12d1ed..5d8d5653b 100644 --- a/apps/opencs/model/filter/narynode.hpp +++ b/apps/opencs/model/filter/narynode.hpp @@ -21,10 +21,6 @@ namespace CSMFilter const Node& operator[] (int index) const; - virtual std::vector getReferencedFilters() const; - ///< Return a list of filters that are used by this node (and must be passed as - /// otherFilters when calling test). - virtual std::vector getReferencedColumns() const; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. @@ -32,7 +28,6 @@ namespace CSMFilter virtual bool isSimple() const; ///< \return Can this filter be displayed in simple mode. - virtual bool hasUserValue() const; }; } diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp index 9743034ba..6783c3b5e 100644 --- a/apps/opencs/model/filter/node.hpp +++ b/apps/opencs/model/filter/node.hpp @@ -33,16 +33,10 @@ namespace CSMFilter virtual ~Node(); virtual bool test (const CSMWorld::IdTable& table, int row, - const std::map& otherFilters, - const std::map& columns, - const std::string& userValue) const = 0; + const std::map& columns) const = 0; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping - virtual std::vector getReferencedFilters() const = 0; - ///< Return a list of filters that are used by this node (and must be passed as - /// otherFilters when calling test). - virtual std::vector getReferencedColumns() const = 0; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. @@ -50,8 +44,6 @@ namespace CSMFilter virtual bool isSimple() const = 0; ///< \return Can this filter be displayed in simple mode. - virtual bool hasUserValue() const = 0; - virtual std::string toString (bool numericColumns) const = 0; ///< Return a string that represents this node. /// diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp index 490d95376..d687b6468 100644 --- a/apps/opencs/model/filter/unarynode.cpp +++ b/apps/opencs/model/filter/unarynode.cpp @@ -13,11 +13,6 @@ CSMFilter::Node& CSMFilter::UnaryNode::getChild() return *mChild; } -std::vector CSMFilter::UnaryNode::getReferencedFilters() const -{ - return mChild->getReferencedFilters(); -} - std::vector CSMFilter::UnaryNode::getReferencedColumns() const { return mChild->getReferencedColumns(); @@ -28,7 +23,3 @@ bool CSMFilter::UnaryNode::isSimple() const return false; } -bool CSMFilter::UnaryNode::hasUserValue() const -{ - return mChild->hasUserValue(); -} diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp index 4e1fb0cc2..6483a730f 100644 --- a/apps/opencs/model/filter/unarynode.hpp +++ b/apps/opencs/model/filter/unarynode.hpp @@ -19,10 +19,6 @@ namespace CSMFilter Node& getChild(); - virtual std::vector getReferencedFilters() const; - ///< Return a list of filters that are used by this node (and must be passed as - /// otherFilters when calling test). - virtual std::vector getReferencedColumns() const; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. @@ -30,7 +26,6 @@ namespace CSMFilter virtual bool isSimple() const; ///< \return Can this filter be displayed in simple mode. - virtual bool hasUserValue() const; }; } diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index 4bb440ce7..2b757adfe 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -27,10 +27,8 @@ bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelI if (!mFilter) return true; - std::map otherFilters; /// \todo get other filters; - return mFilter->test ( - dynamic_cast (*sourceModel()), sourceRow, otherFilters, mColumnMap, mUserValue); + dynamic_cast (*sourceModel()), sourceRow, mColumnMap); } CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) @@ -42,11 +40,9 @@ QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, i return mapFromSource (dynamic_cast (*sourceModel()).getModelIndex (id, column)); } -void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter, - const std::string& userValue) +void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter) { mFilter = filter; - mUserValue = userValue; updateColumnMap(); invalidateFilter(); } \ No newline at end of file diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 40bf6d4d9..b63dccd5e 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -18,7 +18,6 @@ namespace CSMWorld Q_OBJECT boost::shared_ptr mFilter; - std::string mUserValue; std::map mColumnMap; // column ID, column index in this model (or -1) private: @@ -33,8 +32,7 @@ namespace CSMWorld virtual QModelIndex getModelIndex (const std::string& id, int column) const; - void setFilter (const boost::shared_ptr& filter, - const std::string& userValue); + void setFilter (const boost::shared_ptr& filter); }; } diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index cc84e612d..75cbddc2a 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -294,8 +294,7 @@ void CSVWorld::Table::requestFocus (const std::string& id) scrollTo (index, QAbstractItemView::PositionAtTop); } -void CSVWorld::Table::recordFilterChanged (boost::shared_ptr filter, - const std::string& userValue) +void CSVWorld::Table::recordFilterChanged (boost::shared_ptr filter) { - mProxyModel->setFilter (filter, userValue); + mProxyModel->setFilter (filter); } \ No newline at end of file diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 2e24e46a5..d93109056 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -87,8 +87,7 @@ namespace CSVWorld void requestFocus (const std::string& id); - void recordFilterChanged (boost::shared_ptr filter, - const std::string& userValue); + void recordFilterChanged (boost::shared_ptr filter); }; } From 1adce8afb30743d3dfd99d2d8a0112c058f4080a Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 22 Aug 2013 10:47:15 +0200 Subject: [PATCH 078/194] Fix ambient light getting set from the cell data for non-interior cells --- apps/openmw/mwrender/renderingmanager.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 32ddd8b5f..ade871a94 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -547,7 +547,8 @@ void RenderingManager::setAmbientMode() void RenderingManager::configureAmbient(MWWorld::Ptr::CellStore &mCell) { - mAmbientColor.setAsABGR (mCell.mCell->mAmbi.mAmbient); + if (mCell.mCell->mData.mFlags & ESM::Cell::Interior) + mAmbientColor.setAsABGR (mCell.mCell->mAmbi.mAmbient); setAmbientMode(); // Create a "sun" that shines light downwards. It doesn't look @@ -555,12 +556,15 @@ void RenderingManager::configureAmbient(MWWorld::Ptr::CellStore &mCell) if(!mSun) { mSun = mRendering.getScene()->createLight(); + mSun->setType(Ogre::Light::LT_DIRECTIONAL); + } + if (mCell.mCell->mData.mFlags & ESM::Cell::Interior) + { + Ogre::ColourValue colour; + colour.setAsABGR (mCell.mCell->mAmbi.mSunlight); + mSun->setDiffuseColour (colour); + mSun->setDirection(0,-1,0); } - Ogre::ColourValue colour; - colour.setAsABGR (mCell.mCell->mAmbi.mSunlight); - mSun->setDiffuseColour (colour); - mSun->setType(Ogre::Light::LT_DIRECTIONAL); - mSun->setDirection(0,-1,0); } // Switch through lighting modes. From 22d559808256e74ea88fa68bbcfc690edefc635a Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 22 Aug 2013 12:17:12 +0200 Subject: [PATCH 079/194] Fix bad_cast exception when hitting creatures --- apps/openmw/mwclass/npc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 5c46ee75e..d9c1da77b 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -339,7 +339,7 @@ namespace MWClass const MWWorld::Class &othercls = MWWorld::Class::get(victim); if(!othercls.isActor()) // Can't hit non-actors return; - MWMechanics::CreatureStats &otherstats = getCreatureStats(victim); + MWMechanics::CreatureStats &otherstats = victim.getClass().getCreatureStats(victim); if(otherstats.isDead()) // Can't hit dead actors return; From 806e9a28880a9379c5bcc4c3a906dbc1538b6a4b Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 13:14:35 +0200 Subject: [PATCH 080/194] added and and or filter nodes --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/filter/andnode.cpp | 20 +++++ apps/opencs/model/filter/andnode.hpp | 23 ++++++ apps/opencs/model/filter/narynode.cpp | 31 +++++++- apps/opencs/model/filter/narynode.hpp | 9 ++- apps/opencs/model/filter/ornode.cpp | 20 +++++ apps/opencs/model/filter/ornode.hpp | 23 ++++++ apps/opencs/model/filter/parser.cpp | 82 +++++++++++++++++++++ apps/opencs/model/filter/parser.hpp | 2 + apps/opencs/view/filter/editwidget.cpp | 2 +- apps/opencs/view/filter/editwidget.hpp | 3 +- apps/opencs/view/filter/filterbox.cpp | 4 +- apps/opencs/view/filter/filterbox.hpp | 3 +- apps/opencs/view/filter/recordfilterbox.cpp | 4 +- apps/opencs/view/filter/recordfilterbox.hpp | 3 +- apps/opencs/view/world/tablesubview.cpp | 4 +- 16 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 apps/opencs/model/filter/andnode.cpp create mode 100644 apps/opencs/model/filter/andnode.hpp create mode 100644 apps/opencs/model/filter/ornode.cpp create mode 100644 apps/opencs/model/filter/ornode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 031c650df..9fb6c1be3 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode parser + node unarynode narynode leafnode booleannode parser andnode ornode ) opencs_hdrs_noqt (model/filter diff --git a/apps/opencs/model/filter/andnode.cpp b/apps/opencs/model/filter/andnode.cpp new file mode 100644 index 000000000..dfaa56e78 --- /dev/null +++ b/apps/opencs/model/filter/andnode.cpp @@ -0,0 +1,20 @@ + +#include "andnode.hpp" + +#include + +CSMFilter::AndNode::AndNode (const std::vector >& nodes) +: NAryNode (nodes, "and") +{} + +bool CSMFilter::AndNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + int size = getSize(); + + for (int i=0; i >& nodes); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp index cc131d8ac..b96e7d4b2 100644 --- a/apps/opencs/model/filter/narynode.cpp +++ b/apps/opencs/model/filter/narynode.cpp @@ -1,8 +1,11 @@ #include "narynode.hpp" -CSMFilter::NAryNode::NAryNode (const std::vector >& nodes) -: mNodes (nodes) +#include + +CSMFilter::NAryNode::NAryNode (const std::vector >& nodes, + const std::string& name) +: mNodes (nodes), mName (name) {} int CSMFilter::NAryNode::getSize() const @@ -30,6 +33,30 @@ std::vector CSMFilter::NAryNode::getReferencedColumns() const return columns; } +std::string CSMFilter::NAryNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << mName << " ("; + + bool first = true; + int size = getSize(); + + for (int i=0; i +#include #include @@ -12,10 +13,11 @@ namespace CSMFilter class NAryNode : public Node { std::vector > mNodes; + std::string mName; public: - NAryNode (const std::vector >& nodes); + NAryNode (const std::vector >& nodes, const std::string& name); int getSize() const; @@ -25,6 +27,11 @@ namespace CSMFilter ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + virtual bool isSimple() const; ///< \return Can this filter be displayed in simple mode. diff --git a/apps/opencs/model/filter/ornode.cpp b/apps/opencs/model/filter/ornode.cpp new file mode 100644 index 000000000..4fc34e1d5 --- /dev/null +++ b/apps/opencs/model/filter/ornode.cpp @@ -0,0 +1,20 @@ + +#include "ornode.hpp" + +#include + +CSMFilter::OrNode::OrNode (const std::vector >& nodes) +: NAryNode (nodes, "or") +{} + +bool CSMFilter::OrNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + int size = getSize(); + + for (int i=0; i >& nodes); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 305bf7a7d..a35c4bdfd 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -8,6 +8,8 @@ #include #include "booleannode.hpp" +#include "ornode.hpp" +#include "andnode.hpp" namespace CSMFilter { @@ -209,12 +211,89 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() boost::shared_ptr CSMFilter::Parser::parseImp() { + if (Token token = getNextToken()) + { + switch (token.mType) + { + case Token::Type_Keyword_True: + + return boost::shared_ptr (new BooleanNode (true)); + + case Token::Type_Keyword_False: + + return boost::shared_ptr (new BooleanNode (false)); + + case Token::Type_Keyword_And: + case Token::Type_Keyword_Or: + return parseNAry (token); + case Token::Type_EOS: + + return boost::shared_ptr(); + + default: + + error(); + } + } return boost::shared_ptr(); } +boost::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyword) +{ + std::vector > nodes; + + Token token = getNextToken(); + + if (!token || token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + for (;;) + { + boost::shared_ptr node = parseImp(); + + if (mError) + return boost::shared_ptr(); + + if (!node.get()) + { + error(); + return boost::shared_ptr(); + } + + nodes.push_back (node); + + Token token = getNextToken(); + + if (!token || (token.mType!=Token::Type_Close && token.mType!=Token::Type_Comma)) + { + error(); + return boost::shared_ptr(); + } + + if (token.mType==Token::Type_Close) + break; + } + + if (nodes.empty()) + { + error(); + return boost::shared_ptr(); + } + + switch (keyword.mType) + { + case Token::Type_Keyword_And: return boost::shared_ptr (new AndNode (nodes)); + case Token::Type_Keyword_Or: return boost::shared_ptr (new OrNode (nodes)); + default: error(); return boost::shared_ptr(); + } +} + void CSMFilter::Parser::error() { mError = true; @@ -235,6 +314,9 @@ bool CSMFilter::Parser::parse (const std::string& filter) if (mError) return false; + if (getNextToken()!=Token (Token::Type_EOS)) + return false; + if (node) mFilter = node; else diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 4341fae9f..d9f14b5d9 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -28,6 +28,8 @@ namespace CSMFilter boost::shared_ptr parseImp(); ///< Will return a null-pointer, if there is nothing more to parse. + boost::shared_ptr parseNAry (const Token& keyword); + void error(); public: diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 793e0504e..b691a5e16 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -13,7 +13,7 @@ void CSVFilter::EditWidget::textChanged (const QString& text) if (mParser.parse (text.toUtf8().constData())) { setPalette (mPalette); - emit filterChanged (mParser.getFilter(), ""); + emit filterChanged (mParser.getFilter()); } else { diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index 4e692fc4d..76b484de9 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -24,8 +24,7 @@ namespace CSVFilter signals: - void filterChanged (boost::shared_ptr filter, - const std::string& userValue); + void filterChanged (boost::shared_ptr filter); private slots: diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index 9da08d3ba..495abf871 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -19,6 +19,6 @@ CSVFilter::FilterBox::FilterBox (QWidget *parent) setLayout (layout); connect (recordFilterBox, - SIGNAL (filterChanged (boost::shared_ptr, const std::string&)), - this, SIGNAL (recordFilterChanged (boost::shared_ptr, const std::string&))); + SIGNAL (filterChanged (boost::shared_ptr)), + this, SIGNAL (recordFilterChanged (boost::shared_ptr))); } \ No newline at end of file diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index 5f260003a..d3806876d 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -17,8 +17,7 @@ namespace CSVFilter signals: - void recordFilterChanged (boost::shared_ptr filter, - const std::string& userValue); + void recordFilterChanged (boost::shared_ptr filter); }; } diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index c4b516f03..3b5f73f47 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -22,6 +22,6 @@ CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) setLayout (layout); connect ( - editWidget, SIGNAL (filterChanged (boost::shared_ptr, const std::string&)), - this, SIGNAL (filterChanged (boost::shared_ptr, const std::string&))); + editWidget, SIGNAL (filterChanged (boost::shared_ptr)), + this, SIGNAL (filterChanged (boost::shared_ptr))); } diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 8fc1e263b..64c1848a8 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -21,8 +21,7 @@ namespace CSVFilter signals: - void filterChanged (boost::shared_ptr filter, - const std::string& userValue); + void filterChanged (boost::shared_ptr filter); }; } diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index cb6f430c1..a43ae2dac 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -52,8 +52,8 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D mTable, SLOT (requestFocus (const std::string&))); connect (filterBox, - SIGNAL (recordFilterChanged (boost::shared_ptr, const std::string&)), - mTable, SLOT (recordFilterChanged (boost::shared_ptr, const std::string&))); + SIGNAL (recordFilterChanged (boost::shared_ptr)), + mTable, SLOT (recordFilterChanged (boost::shared_ptr))); } void CSVWorld::TableSubView::setEditLock (bool locked) From ba6edc55d457f62c9f85850f48278ee49d787187 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 13:45:50 +0200 Subject: [PATCH 081/194] added not filter node --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/filter/notnode.cpp | 10 ++++++++++ apps/opencs/model/filter/notnode.hpp | 21 +++++++++++++++++++++ apps/opencs/model/filter/parser.cpp | 24 ++++++++++++++++-------- apps/opencs/model/filter/parser.hpp | 2 +- apps/opencs/model/filter/unarynode.cpp | 8 +++++++- apps/opencs/model/filter/unarynode.hpp | 11 ++++++++--- 7 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 apps/opencs/model/filter/notnode.cpp create mode 100644 apps/opencs/model/filter/notnode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 9fb6c1be3..c74f0db22 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode parser andnode ornode + node unarynode narynode leafnode booleannode parser andnode ornode notnode ) opencs_hdrs_noqt (model/filter diff --git a/apps/opencs/model/filter/notnode.cpp b/apps/opencs/model/filter/notnode.cpp new file mode 100644 index 000000000..1b22ea7a6 --- /dev/null +++ b/apps/opencs/model/filter/notnode.cpp @@ -0,0 +1,10 @@ + +#include "notnode.hpp" + +CSMFilter::NotNode::NotNode (boost::shared_ptr child) : UnaryNode (child, "not") {} + +bool CSMFilter::NotNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + return !getChild().test (table, row, columns); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/notnode.hpp b/apps/opencs/model/filter/notnode.hpp new file mode 100644 index 000000000..b9e80b8c6 --- /dev/null +++ b/apps/opencs/model/filter/notnode.hpp @@ -0,0 +1,21 @@ +#ifndef CSM_FILTER_NOTNODE_H +#define CSM_FILTER_NOTNODE_H + +#include "unarynode.hpp" + +namespace CSMFilter +{ + class NotNode : public UnaryNode + { + public: + + NotNode (boost::shared_ptr child); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index a35c4bdfd..5dd0392e8 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -10,6 +10,7 @@ #include "booleannode.hpp" #include "ornode.hpp" #include "andnode.hpp" +#include "notnode.hpp" namespace CSMFilter { @@ -209,7 +210,7 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() return Token (Token::Type_None); } -boost::shared_ptr CSMFilter::Parser::parseImp() +boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty) { if (Token token = getNextToken()) { @@ -228,8 +229,21 @@ boost::shared_ptr CSMFilter::Parser::parseImp() return parseNAry (token); + case Token::Type_Keyword_Not: + { + boost::shared_ptr node = parseImp(); + + if (mError) + return boost::shared_ptr(); + + return boost::shared_ptr (new NotNode (node)); + } + case Token::Type_EOS: + if (!allowEmpty) + error(); + return boost::shared_ptr(); default: @@ -260,12 +274,6 @@ boost::shared_ptr CSMFilter::Parser::parseNAry (const Token& ke if (mError) return boost::shared_ptr(); - if (!node.get()) - { - error(); - return boost::shared_ptr(); - } - nodes.push_back (node); Token token = getNextToken(); @@ -309,7 +317,7 @@ bool CSMFilter::Parser::parse (const std::string& filter) mInput = filter; mIndex = 0; - boost::shared_ptr node = parseImp(); + boost::shared_ptr node = parseImp (true); if (mError) return false; diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index d9f14b5d9..13c77b09a 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -25,7 +25,7 @@ namespace CSMFilter Token checkKeywords (const Token& token); ///< Turn string token into keyword token, if possible. - boost::shared_ptr parseImp(); + boost::shared_ptr parseImp (bool allowEmpty = false); ///< Will return a null-pointer, if there is nothing more to parse. boost::shared_ptr parseNAry (const Token& keyword); diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp index d687b6468..d1897f4b7 100644 --- a/apps/opencs/model/filter/unarynode.cpp +++ b/apps/opencs/model/filter/unarynode.cpp @@ -1,7 +1,9 @@ #include "unarynode.hpp" -CSMFilter::UnaryNode::UnaryNode (boost::shared_ptr child) : mChild (child) {} +CSMFilter::UnaryNode::UnaryNode (boost::shared_ptr child, const std::string& name) +: mChild (child), mName (name) +{} const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const { @@ -23,3 +25,7 @@ bool CSMFilter::UnaryNode::isSimple() const return false; } +std::string CSMFilter::UnaryNode::toString (bool numericColumns) const +{ + return mName + " " + mChild->toString (numericColumns); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp index 6483a730f..9fd5faf3f 100644 --- a/apps/opencs/model/filter/unarynode.hpp +++ b/apps/opencs/model/filter/unarynode.hpp @@ -1,5 +1,5 @@ -#ifndef CSM_FILTER_UNARIYNODE_H -#define CSM_FILTER_UNARIYNODE_H +#ifndef CSM_FILTER_UNARYNODE_H +#define CSM_FILTER_UNARYNODE_H #include @@ -10,10 +10,11 @@ namespace CSMFilter class UnaryNode : public Node { boost::shared_ptr mChild; + std::string mName; public: - UnaryNode (boost::shared_ptr child); + UnaryNode (boost::shared_ptr child, const std::string& name); const Node& getChild() const; @@ -26,6 +27,10 @@ namespace CSMFilter virtual bool isSimple() const; ///< \return Can this filter be displayed in simple mode. + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } From decd826208ef06553fe72fd374d50b3af9627742 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 14:00:55 +0200 Subject: [PATCH 082/194] fixed an include guard --- apps/opencs/model/filter/leafnode.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp index 2b0163065..a9790d01d 100644 --- a/apps/opencs/model/filter/leafnode.hpp +++ b/apps/opencs/model/filter/leafnode.hpp @@ -1,5 +1,5 @@ -#ifndef CSM_FILTER_UNARIYNODE_H -#define CSM_FILTER_UNARIYNODE_H +#ifndef CSM_FILTER_LEAFNODE_H +#define CSM_FILTER_LEAFNODE_H #include From de956737fe06a2ef516943ef3d48b847f6adddc5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 14:50:42 +0200 Subject: [PATCH 083/194] added text filter node --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/filter/parser.cpp | 77 ++++++++++++++++++++++++++- apps/opencs/model/filter/parser.hpp | 2 + apps/opencs/model/filter/textnode.cpp | 61 +++++++++++++++++++++ apps/opencs/model/filter/textnode.hpp | 33 ++++++++++++ 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 apps/opencs/model/filter/textnode.cpp create mode 100644 apps/opencs/model/filter/textnode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index c74f0db22..ea78135b9 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode parser andnode ornode notnode + node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode ) opencs_hdrs_noqt (model/filter diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 5dd0392e8..c2562ad1e 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -7,10 +7,13 @@ #include +#include "../world/columns.hpp" + #include "booleannode.hpp" #include "ornode.hpp" #include "andnode.hpp" #include "notnode.hpp" +#include "textnode.hpp" namespace CSMFilter { @@ -31,7 +34,8 @@ namespace CSMFilter Type_Keyword_False, Type_Keyword_And, Type_Keyword_Or, - Type_Keyword_Not + Type_Keyword_Not, + Type_Keyword_Text }; Type mType; @@ -162,6 +166,7 @@ CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) { "true", "false", "and", "or", "not", + "text", 0 }; @@ -239,6 +244,10 @@ boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty) return boost::shared_ptr (new NotNode (node)); } + case Token::Type_Keyword_Text: + + return parseText(); + case Token::Type_EOS: if (!allowEmpty) @@ -261,7 +270,7 @@ boost::shared_ptr CSMFilter::Parser::parseNAry (const Token& ke Token token = getNextToken(); - if (!token || token.mType!=Token::Type_Open) + if (token.mType!=Token::Type_Open) { error(); return boost::shared_ptr(); @@ -302,6 +311,70 @@ boost::shared_ptr CSMFilter::Parser::parseNAry (const Token& ke } } +boost::shared_ptr CSMFilter::Parser::parseText() +{ + Token token = getNextToken(); + + if (token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (!token) + return boost::shared_ptr(); + + // parse column ID + int columnId = -1; + + if (token.mType==Token::Type_Number) + { + if (static_cast (token.mNumber)==token.mNumber) + columnId = static_cast (token.mNumber); + } + else if (token.mType==Token::Type_String) + { + columnId = CSMWorld::Columns::getId (token.mString); + } + + if (columnId<0) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + // parse text pattern + token = getNextToken(); + + if (token.mType!=Token::Type_String) + { + error(); + return boost::shared_ptr(); + } + + std::string text = token.mString; + + token = getNextToken(); + + if (token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + + return boost::shared_ptr (new TextNode (columnId, text)); +} + void CSMFilter::Parser::error() { mError = true; diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 13c77b09a..2512de141 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -30,6 +30,8 @@ namespace CSMFilter boost::shared_ptr parseNAry (const Token& keyword); + boost::shared_ptr parseText(); + void error(); public: diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp new file mode 100644 index 000000000..f95c1ce28 --- /dev/null +++ b/apps/opencs/model/filter/textnode.cpp @@ -0,0 +1,61 @@ + +#include "textnode.hpp" + +#include +#include + +#include + +#include "../world/columns.hpp" +#include "../world/idtable.hpp" + +CSMFilter::TextNode::TextNode (int columnId, const std::string& text) +: mColumnId (columnId), mText (text) +{} + +bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + const std::map::const_iterator iter = columns.find (mColumnId); + + if (iter==columns.end()) + throw std::logic_error ("invalid column in test node test"); + + if (iter->second==-1) + return true; + + QModelIndex index = table.index (row, iter->second); + + QVariant data = table.data (index); + + if (data.type()!=QVariant::String) + return false; + + QRegExp regExp(QString::fromUtf8 (mText.c_str())); /// \todo make pattern syntax configurable + + return regExp.exactMatch (data.toString()); +} + +std::vector CSMFilter::TextNode::getReferencedColumns() const +{ + return std::vector (1, mColumnId); +} + +std::string CSMFilter::TextNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << "text ("; + + if (numericColumns) + stream << mColumnId; + else + stream + << "\"" + << CSMWorld::Columns::getName (static_cast (mColumnId)) + << "\""; + + stream << ", \"" << mText << "\")"; + + return stream.str(); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/textnode.hpp b/apps/opencs/model/filter/textnode.hpp new file mode 100644 index 000000000..663fa7382 --- /dev/null +++ b/apps/opencs/model/filter/textnode.hpp @@ -0,0 +1,33 @@ +#ifndef CSM_FILTER_TEXTNODE_H +#define CSM_FILTER_TEXTNODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class TextNode : public LeafNode + { + int mColumnId; + std::string mText; + + public: + + TextNode (int columnId, const std::string& text); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif From f615a9397bd85643804a332cd0d71b0a65a514ea Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 15:16:22 +0200 Subject: [PATCH 084/194] made text node filter case-insensitive --- apps/opencs/model/filter/textnode.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index f95c1ce28..ec654267b 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -31,7 +31,8 @@ bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, if (data.type()!=QVariant::String) return false; - QRegExp regExp(QString::fromUtf8 (mText.c_str())); /// \todo make pattern syntax configurable + /// \todo make pattern syntax configurable + QRegExp regExp (QString::fromUtf8 (mText.c_str()), Qt::CaseInsensitive); return regExp.exactMatch (data.toString()); } From 63b1df85e75fb0f9418993be50b3c1b30fce4b2d Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 22 Aug 2013 15:22:39 +0200 Subject: [PATCH 085/194] fixed string parsing --- apps/opencs/model/filter/parser.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index c2562ad1e..7c8f9d768 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -113,6 +113,9 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() error(); return Token (Token::Type_None); } + + if (string[0]=='"') + string = string.substr (1, string.size()-2); } return checkKeywords (string); From 717e04cac5be057beb0fe4199293509f63edcc75 Mon Sep 17 00:00:00 2001 From: Marek Kochanowicz Date: Fri, 23 Aug 2013 12:48:34 +0200 Subject: [PATCH 086/194] More icons. At last! --- files/opencs/raster/.directory | 7 +- files/opencs/raster/Info.png | Bin 0 -> 1234 bytes files/opencs/raster/LandTexture.png | Bin 0 -> 2662 bytes files/opencs/raster/PathGrid.png | Bin 0 -> 1297 bytes files/opencs/raster/birthsign.png | Bin 0 -> 2454 bytes files/opencs/raster/body-part.png | Bin 1359 -> 1788 bytes files/opencs/raster/cell.png | Bin 0 -> 1403 bytes files/opencs/raster/class.png | Bin 0 -> 2283 bytes files/opencs/raster/enchantment.png | Bin 0 -> 1812 bytes files/opencs/raster/faction.png | Bin 0 -> 1858 bytes files/opencs/raster/globvar.png | Bin 0 -> 2394 bytes files/opencs/raster/land.png | Bin 0 -> 1220 bytes files/opencs/raster/landpaint.png | Bin 0 -> 1361 bytes files/opencs/raster/magic-effect.png | Bin 0 -> 1702 bytes files/opencs/raster/magicrabbit.png | Bin 0 -> 1820 bytes files/opencs/raster/map.png | Bin 0 -> 1477 bytes files/opencs/raster/race.png | Bin 0 -> 1834 bytes files/opencs/raster/script.png | Bin 0 -> 952 bytes files/opencs/raster/skill.png | Bin 0 -> 1676 bytes files/opencs/raster/spell.png | Bin 0 -> 2071 bytes .../scalable/referenceable-record/.directory | 8 +- .../referenceable-record/activator.svg | 96 +++--------------- 22 files changed, 25 insertions(+), 86 deletions(-) create mode 100644 files/opencs/raster/Info.png create mode 100644 files/opencs/raster/LandTexture.png create mode 100644 files/opencs/raster/PathGrid.png create mode 100644 files/opencs/raster/birthsign.png create mode 100644 files/opencs/raster/cell.png create mode 100644 files/opencs/raster/class.png create mode 100644 files/opencs/raster/enchantment.png create mode 100644 files/opencs/raster/faction.png create mode 100644 files/opencs/raster/globvar.png create mode 100644 files/opencs/raster/land.png create mode 100644 files/opencs/raster/landpaint.png create mode 100644 files/opencs/raster/magic-effect.png create mode 100644 files/opencs/raster/magicrabbit.png create mode 100644 files/opencs/raster/map.png create mode 100644 files/opencs/raster/race.png create mode 100644 files/opencs/raster/script.png create mode 100644 files/opencs/raster/skill.png create mode 100644 files/opencs/raster/spell.png diff --git a/files/opencs/raster/.directory b/files/opencs/raster/.directory index 7a78deef5..a68c37e30 100644 --- a/files/opencs/raster/.directory +++ b/files/opencs/raster/.directory @@ -1,5 +1,8 @@ [Dolphin] +GroupedSorting=true PreviewsShown=true -Timestamp=2013,3,31,10,12,5 +SortFoldersFirst=false +Timestamp=2013,8,23,12,44,33 Version=3 -ViewMode=1 +ViewMode=2 +VisibleRoles=Compact_text,Compact_size diff --git a/files/opencs/raster/Info.png b/files/opencs/raster/Info.png new file mode 100644 index 0000000000000000000000000000000000000000..d7bdad6cb1699891de65fecd17cd3708daa198bf GIT binary patch literal 1234 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 U5S+aqrvLx|07*qoM6N<$f?VGlaR2}S literal 0 HcmV?d00001 diff --git a/files/opencs/raster/PathGrid.png b/files/opencs/raster/PathGrid.png new file mode 100644 index 0000000000000000000000000000000000000000..23b6b84d74867d65d7eb4542c5bc2864c94ea0ef GIT binary patch literal 1297 zcmV+s1@8KZP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000DlNkl&1h@qpg`~OoL(;U&%JFjqc8IC%f0>1{m%J)=R3diePsZcO~ic( zJjC(Gh?oS(wt-le0Qr`hOXD2F2L^nKfvLq|#8cZZOV1^rOl*(9UIq$g;Pd09KiG~P z{aLDztS^?&5~S=GUMc&-QeJ+kKoLt9;b2+W*WVsG1a3sFLUAY8Nh6Mt{W_vX#Qx5a zBXjrm?}sxoSyCrSf>*&B0_Bno_62$TE;_hfQUZ<79wnV;ebZ7UPfVlz8$>O>XWE0o z10$vid2$}--b8ylWMH*3I0(=+zS3BVzfx;OejrUi|etDWcK!r(4 zA-NVSLON2n_j>n9!Aa*NwN9msj_a&%1UvNsL%JF5+Nn63P#x3bsB&mST z{5%4*azW=LwcKwgfRtSz;6nnJk$^^;z~h3gm{u;O^J0B~>>AvH*9&jKKf4bU>jQlE zle*0OA1@@dE!}qe%ez_m53giai)HlP-rj^>uTL~KG^ltIMF_l)MM%{ycG!>uvJ=Gi z`3pS2Y7goh4NYOm;uwbWWe3`F(hb{=RP?^x5r@OeWL8mzS3z91g>Gw{AgK zXFWD3Cc${U&o3EU*(cFx6xP<(!0-3NmEmDHYqxEDh)@muNY0E#<0ux(lQRxS3JIld<5tk5%-|l62TVre zZy7QYC4lqiXr8d=UauF>cS>DX$6?@gtwuAty@2~W1ZW`5ytz4Gx7jw0t*vRM!pbSQ z%jF{GF(adEevEUipeG*I7Wu5+M1eh&B>F{a7xDw zUAze69*0kGo_#A}wOT(Ya34r#Aj;Q)C_>aHE`9vcUs_Fd zDjF?$fWPT(ARzDWdn{HGx4c=;UA_7m{cWc!f9ChO@0))EwEBWcff-P}00000NkvXX Hu0mjfN03_( literal 0 HcmV?d00001 diff --git a/files/opencs/raster/birthsign.png b/files/opencs/raster/birthsign.png new file mode 100644 index 0000000000000000000000000000000000000000..8192d2ebf86f46bf1a63d47b9e689957c45236c7 GIT binary patch literal 2454 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 delta 1252 zcmVjNHRL`KTaA0#JC0a)(<=vGx21XjBX~#~xvQ@LMEAg_JIlBQ6=US=It!Hbz4m&eOT!;$B#^?iVj5Q+ROc(*Ui6DiaW38Y?}8taB=C>dZ!b zn2%imJZei)uszvAX05?mUm4bVukXfgcK)j4OT^g&Kz}B7*WN8mCe3fgh10=o?9{nA zcC+(^woJrn6`(tT>vX*GT(Hc+nsFRDg403Dr0)wp!PaCunS0>A*v-ygHm4&tQ~{}g zH{1SoEIjz!N!tJfDw1mQE@E?{g>62axn=BT=W|V|1i%~7fy~B>hD*??B=a@_AX~0_ zvgH~n-hWSI?*6Q}W$b3>vyGP!tC1r=DFkzm(-7|CVJm=B)pX_?V~@$))vUN>>}KaL z8d6w*``M9r+5EZf02mR!X{DzKZ$}ykfG;a<8N1o}^ur_s$%L3`_zZJRmkEH_HVODK zBI8O=3D$?|QI(}<#cAx+ITfeQsd!s)G-5&+%zw9hK}L{@Fr~;g0F20WV7a@HETH=b ziuYF;P?4sERw+g+fmr&b0F(C&@Rzz-buS!x3`3NXk)>Y?@n-Pe9_KeRA`rj`5#cLA zMQSM2Qa9H5LTeVrs?I_q7fjjY6hx?*`DJ4%R(o&l#-14)z!q+*?jj~@4ahj>i*&t; z6@Q2O#pNRz|@(8*Bw{cwwq&rem9O4FHZ^F z_^YyuD< z6Pl>_>y9gI;iz*ePGk2L@^)uVom26_vLL7>E>;~utNs;xi(QB5ZZ|!-wnv;6E>X*{ zg|ifYa>Wl~-U+L2LaP860k5jPRFB1<^LCwEw%tkhya@tQEHkfW9OX@N(0meK1|0jM3X?_Qg(}4;1;KIF5ma z;V0P~fPT>V#PM;uWd0&I-4L0(xo~Fj{(KdR;=Blet9=0Y9H(kpxP{hCHU-*tOYz=3 z1qx$4q49FD4}gewvb-@X2n)>_7+oSq?p^WUbL1#EFR>p0T`WFgm+c!#z6Xj|CU`<6 z;{HoZYnKkNWpw8{27B>yltO2tQpjZ=g@9`ma-ANKr<~!RbUcbUefK|iFCsaz41%&#EP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000E)Nkl7ip7{|GL!GmU5b~|71 zeC^K8$L{QOXFp3zmqMYiu=Go@g;L`@FOuHDjH)v=)sFe z4hB6m#xy35AsiGnXkz}(3@&!t?Z=h_G2t+mndfqGJ|?%3PQ$c7~+MY{LSG$>qfDB1nq+aBecO$Ys8j zhcqNXPVhMQWj`iA?ZWYEotXGw3--K{fnMNmc-He7azz`{pdHr0#o5p2i&%6E{!N}u zy_N#sO75^aoAZ*r_f}E-UM4MfMIV4h?^7bvI&0=rj2Y((*eg?5AxECxZ66uQsHQLWsAZ<|uF zX;OjMN|3pf0Xx8gHmET2Dr#y4{ zt9^*INxpJ^nD>+EWD|Cs*8Ohpsl?H2hLMgvj(mxreAZY1SBeCY(Fn{b^DewIh(LP) z0Xu-O7@DbyxF;ZAlGSXNJnO^LqQA$=VdcN%VgPJ{!md-g-*ARbA*qJYF)S{CZ#t#G z=;k5h8E_T_AszzJ*Mru=po}n@pmR$B#V)^Qs{U#2GyjIQ38kx>yaOnuwznd2%b4A!7jVUG`b!}BnZtF zpgipsw~qzsWWW&mSAVi43c_@AQM@^LE{XAvGB|WKjf3yoXfHAt&>lgCf2{5~pWATr zEBD~3aS_Qw0^$dGzj6;QmKd-)eUnk>Y2SglN5bxuD)v^%={u9~$7|=Hh;51D!mH`jKxkASxr_cE#vUXg%mkkQIlj5gkBA6NVdw-j7|fI4xd~a z+>n53usez?Y7234qjJqr^D2+;;UT(U!CD!3RRXF>(JWVq<~XN$NQ5~kK(bj!Y-a23 z@pAu+%l;ECwSa816)Ckk@O?S3Tvd&Hzb^+G0WB8$P{0Fs;2$t|&*3H>@<{*y002ov JPDHLkV1m1kiJ|}i literal 0 HcmV?d00001 diff --git a/files/opencs/raster/class.png b/files/opencs/raster/class.png new file mode 100644 index 0000000000000000000000000000000000000000..316380363ae0a1039bbd8810b2a100099503446c GIT binary patch literal 2283 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000PDNklh8OL{=Oonk%Cu3ta z&NQ7g9>@*LF1yQKz}^=N?807LdvL3}$gw~WSP)zg1QZkm!sS6~L@-H}ai|c}z{Do8 zwpeY^G#;&!F;#0@J8_(Uzx@(+LX=8OX1X)q%sb!l{Ql4JKF_-p0F?jd^~~^Jy!P6Q zB9Yo7j8l2M>t}W1dkK(96GH`j$%s@Ee_?blQvuot^KXyL$p19q*#G^*DCzI*Nvd!*II?QCivokx=u1&yy>bD`Hs! zydhS|b$9#vCYOX)R`$Z>+K-~5y@+Ou?k+bi%Mu_K#rHMT_oJ!lFbw)^7!BEQIz2wm zv$D1$US)zzmIAxI270{%^hjRca-y&-0YuLKk}2?hn69JFU!Qr7$a@YlOqsa*z6V9y zoP=Wl@f>f~=cBhShg^GqMTHK0+Np;$MglfVhVh=~pq5bQ|Cfj*1sD@vxj1+FdCb1| zQ^;jnBxq7`d$JClwFRgww8LT3W87oHvHBEjvFK1%kdFOTHvG0d2`r`vv4Q{^Up_%@L`t?%ZBn)h}8X<~e1X~Y7q-5hnF9mAx%eR&# zz@n!Xo*Z0>3zMtCu z_BE_RjdLYrvDCuvig+vnMB&XoxI}@;E(CY)?XTe-cM;uw8!U|zfdL_4(-}BctMhq& zuv7wiSQs{L2tY8;?1Kl{#}JS@1d)t}BYP+i3#hYC72veLbmRUU1(LM1MR0b@-@t0R z4yJ1wQR>YI5063=lZ!|i6CojVgbOnfYCW_F9+-I)0cl6Th!tYEl>&|Qr5jHdkoEH~ zt4rvRYLX!#@-9wufmby8HN4>}qUtV#UOk7%%JZO=okK*)48jZFN7&Y1KM}skIE1j6 z1oZEsz!1Osu{v0k0<}`{Sw(p{jvqgcv9U3XjErDlU;qUL1z_`pU~K<*S#Y+l0@2Z2 zu%jZupoM`E5sbL#KnPd?knw3-z8wXh0JT#7kHNt~OiWBHf_HUwp{=bAwQe_Pw8*D= z&gO~8#Eu~iNUkB0PkF8O@p`q!c4M{vAsio>E-81yL*>~t02PEgD4@ z7EJLW5cAf9&s_JJMa5K56miT;UgLZA>_KyLGcn^Eh|`T5?d|Qv{BMH8;Xqzq9_%(7 z(utixkE9WmPk*f7qM}$ttxz#N?T3kzemF7ehcfZU^{Q3ambeOyZa>tP`Jvq5 z2R?f(VxxjCQSwveQ$vk8sCVda=Aaunwlug)^H5S!Lc)u4%}6uoaI~!mJF~?o)N`Oo z(2`BYyU!Q4n$etrI%f*HsxxqMa0k*&DHzyYhU}0HC>KVczqJzCnI??261Giz?8+6w zETK(N@=VgHe}B}Do2TnhQ|ZFo+#K==j~6`8&B?*Nd-sS*=fq@_4rg1m&}sDu(0BUa z81HF&wI&kP%tRDdMs~Ob_=cozL<(U%yxuA!Zk#B^Uyc>x%18mS6Qg0*urSlE z$0r>rB)rcN114XEP09Nf!Pjd#uuf7-G%E$(YqR6}IAP~)C(ie0!z5$ijr17&uFFJv z{!wcJ8m$}%8Jl?IHe6^3N#dxBe>k|AgkK)c#>F=+xX^9(!DpJ3NQ(8xuF6$V#fL&5 zqJu4r0V75TjvxYZWe`eoe*(RnM#ATh5O#*LFxQv42tHXSgMuC4JD~Ukn3e2MIWPSF z$3503!m||Nj}nBh-YLm>0X?1-WN+{BP|#IRL5G`yM%N114KF|y_xc@&LGWlPIN|yI zBwnEJ-_xR5V3aXJwL;qb4tp$yT{5!Zyx|f@ZMr6;`;#FP^Qi_Ak2)sgQD^0GYo??z zYo=nNsk3}Gb&SWN8ockwxM4gwC-hotfe6E;UOOUGnORVC1LwaDUzCDx4+sf6h?lQn zICIrBr|6qpE$4{+-FGCcAYR@#b{2B})N%YQpda$he*taD)yj+Zn<@YR002ovPDHLk FV1mysJKq2R literal 0 HcmV?d00001 diff --git a/files/opencs/raster/enchantment.png b/files/opencs/raster/enchantment.png new file mode 100644 index 0000000000000000000000000000000000000000..c90fb27ce33fbca5c4a6a10ff2c6f3c265d7ca5f GIT binary patch literal 1812 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 Tq^~E300000NkvXXu0mjf43Bmj literal 0 HcmV?d00001 diff --git a/files/opencs/raster/magic-effect.png b/files/opencs/raster/magic-effect.png new file mode 100644 index 0000000000000000000000000000000000000000..e672ffccb3bc9f8203a7a0ebf8954ab17b5ffbf9 GIT binary patch literal 1702 zcmZ8ic~}x=7^gXPNw_VK%L#WbBqAs(P&9!&^DO>AYJX|*W~@1<#L z&C!-tT$8_xnA+-}^pa&Itqzq@<|?002O6 z4_7}~)cwo8P>^|CN{Uz(w&DC>&HzUL-g#N_c{J1;3INm>0;S>dvV8klk6;`C0NnYP zZ5x_Om&y(`@oqu*lZjDyVt5h;a3UO=fQKO5{B;TD5KG7rQ&VwL6#$^126u(}2f^r+ z^IoGE)kmAP^id}&=oA~>b`29zLpAX+1@~fN3?~9nha>9=?rK*{OC1feS^k z>%wO7xm8{um)@%p7!&E&UAX~ z6wx=K=m0aeeahppLkp-kVlUA$h69Um8H{830-1$9vdoKIrpXPOj)YI*wEC$wn zU?NfTU78@f%i!uOa&Ti-1SXFPh; zw0vfNpv%ac_L93+s5RV+&#+HJu?yi$4L$Ae4VLUNyB&`tg>`gZ$WsW3`NhG;F5B zP42M8pw+p!rD5i<{yifvXrWSI<}jJEuAoA2$g>bqB74r|+x(kC>~we&MeK^OXy3 z4^$JYEpTLOEmpQ?%Ww8{A^0|{kzK3v0ohH3hB|n>kBXy8vALiIj(8*lFS5oF zJj;ZkFFji}YRrX(xM_#Y44Li_&6uhjE6cjM6-`=}0jG~J{zIIl>5Ns$J{-&W=XL1% zZ0m9pe?#M=P8ANvhzc~y)4rs@=eZ>mvp0Jfc6o`t!ErgOt#W4tKV49##hknt;z$g0 z_3zIZzdi-oXnb}PB<$O7?q%J6hO-3o>mK*p94%Mw==@Iy881ZLgB6f77yrxG`Y??*QTg)Z(?NnJu8v%-VzlgAlqUcgbz| zbP=^>VVt(?<^1W0@KQHkzi$AzSCi~k%BhUtJNir8$z)>uu=+~p;E5eYDiy)Jkg)s9 zxogjW{2fz?tjTd&G0E4RKa_DG7ts;#8?7W0ZqBXe!i4;}R%JMaPI4J7weD^E|P&)ZsYZ6Hhl6ffGOrUfNQB)-05It;n!pw>5RJ_)UXo$rH}LqmmXCKcE?uZoI2I4`JOu6(@gi5lW~7P0-&JNe&M0`}?MzXb>{;=| oB_i_=g4g@-x5<4v2-u?2mQo5pLDlek**5}!yCGZ|&SBsE3EgiyE&u=k literal 0 HcmV?d00001 diff --git a/files/opencs/raster/magicrabbit.png b/files/opencs/raster/magicrabbit.png new file mode 100644 index 0000000000000000000000000000000000000000..d1d7c8270b2fec8dd98806f591bf5bf4aa70331d GIT binary patch literal 1820 zcmV+%2jlpOP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000JyNklC^@s{=4Ko|d>FYoIl8d~R<2l~J9zLQu3x_n zoleIDl<%zqywTZ)2D$(h<>koJYFox`p0jFIOWCenc=6%|Dk>`YQIo1`(nGvi(UvX7 zVFFtU3;Fk`ch{|3XJi6KqmkS6#{KaKnM{Di*VosFlP6DN*Up`9WLQ;Ig=0sL;?}KO zNK&bOdhZork*&ze>Suv%-n<$4Yt}TX)oOiaW~N@F(KMD*Ypz_rjD-03zy3?4?_2@i zfPCO~XJ;pRdV28c)hiK$gM%0t7(iE77al%*h>ucJZv+FOs183dRAv= zXAiu7{TgLuWvH*O7lM2K{5k5*oDpq#_Usw*^YdT2xw=-nxw&ci-HFJ&L4d#%GMP+A z(c&$a_k;uoBPuEi2?+_PIduw$Xw=vf8fYp`pFWM++FJ3O*Y@w<4;ErbNC6_xn3x!ly`sOrA2b01kT%6< z#l^y$_&2X@Sic@kO-+bcv+1`-TrSACy0{EFIXOLba&%liV#O>NnU_~v zRAeMbPy#rPz{11B5fl^zsZPefvlRGhXJ;qY_-b!&4+jSa zy#-}pWblZHh!Jq^A-^*rN6nHQR&(p&Lvu4c_;M1p1hoz+@ z3Tgh?Y%2HcK{-tVS0yhm4?DJR$F{9oap1rK(3v1IWMB@?lnJ=IyN?2&GiMGQ?CrZO zNa=~B*gqMRGv_P!J}fLu2#9;g!n=3x9`^0qhpHn-(A(Q9=AK)@m3Z{%k+{Ei?_N|^ zRth&HqY`E3_qp%ItP>6ZwapTBRtN#&mbTp{40@#k* zAQ6J=q^#>GMtQ-wPMEoD?a-mhI;5#`5EGG%(4c4p_yuEuGK};=Gfkr6V6E_Yxw^Q+ zTe<);PbGY$!3dB?BSevcbGpj?6V*?CT&Q6xeq_;jA%I4fx08VqxP$((0%b6 zPV8<&b!jWU-u3`r7LhhK-p1~NX6)27VMq22lw|&b!eu{WL+TZ1BYJ-?a_u1Pq94};di+k-~h!DHO-gK=9xf;8}W1_g#7A}k(DA~O&hxg6@0 z?I>RTJqojHu|ZvfwV&vazw}#ZQ%*qj!6qtqDgu4t2{;rITSxG?kq!)Iw}1{MgG!}> zzrVk5MIK2O8hg42_1#|)jzA^p!Z(t7zmP>X;&iNbHW~(J|FE^OF`odP4~BSB{=dQb zfZ?-&3Gi&Q_;|kAY=&)iEcoo%vteUxZ9V~VNJDIF>`3Ns3(jsz_G>-?@&v=;#fwGR z-vxZej2Ub|<`eKF0{kKJuHe(AO=A}{pMZzEyKxbnfN#@tE<0z=;8Ukg^HL7Y3%_;y|tB9;zVD>eE)AGZZSMjInD?G0000< KMNUMnLSTXwuTk3o literal 0 HcmV?d00001 diff --git a/files/opencs/raster/map.png b/files/opencs/raster/map.png new file mode 100644 index 0000000000000000000000000000000000000000..3653797cca076d1ef7a1983d109e67e2fc4ab52c GIT binary patch literal 1477 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 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/script.png b/files/opencs/raster/script.png new file mode 100644 index 0000000000000000000000000000000000000000..297da40210490e4345d2ef8adda136cc8a404db4 GIT binary patch literal 952 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 B*sA~l literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/referenceable-record/.directory b/files/opencs/scalable/referenceable-record/.directory index e2d80ed58..c633c38a9 100644 --- a/files/opencs/scalable/referenceable-record/.directory +++ b/files/opencs/scalable/referenceable-record/.directory @@ -1,5 +1,7 @@ [Dolphin] -PreviewsShown=true -Timestamp=2013,3,21,10,19,49 +GroupedSorting=true +SortFoldersFirst=false +Timestamp=2013,8,11,16,50,43 Version=3 -ViewMode=1 +ViewMode=2 +VisibleRoles=Compact_text,Compact_size diff --git a/files/opencs/scalable/referenceable-record/activator.svg b/files/opencs/scalable/referenceable-record/activator.svg index 0c6db59a7..e917d9332 100644 --- a/files/opencs/scalable/referenceable-record/activator.svg +++ b/files/opencs/scalable/referenceable-record/activator.svg @@ -23,18 +23,18 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="5.6568542" - inkscape:cx="5.7818736" - inkscape:cy="23.778975" + inkscape:zoom="2" + inkscape:cx="-35.165989" + inkscape:cy="-65.142411" inkscape:document-units="px" - inkscape:current-layer="g3891" + inkscape:current-layer="g3910" showgrid="false" showguides="true" inkscape:guide-bbox="true" inkscape:window-width="1280" - inkscape:window-height="994" + inkscape:window-height="1001" inkscape:window-x="0" - inkscape:window-y="30" + inkscape:window-y="23" inkscape:window-maximized="1" inkscape:snap-page="true" inkscape:snap-bbox="true" @@ -45,7 +45,7 @@ inkscape:object-paths="true" inkscape:snap-intersection-paths="false" inkscape:object-nodes="true" - inkscape:snap-global="true" + inkscape:snap-global="false" inkscape:snap-smooth-nodes="true" inkscape:snap-grids="false" inkscape:snap-nodes="true" /> @@ -867,7 +867,7 @@ xlink:href="#linearGradient3942" id="linearGradient3955" gradientUnits="userSpaceOnUse" - gradientTransform="translate(-183.1468,-158.28118)" + gradientTransform="translate(-445.4059,-219.9852)" x1="74.006111" y1="153.75172" x2="344.5" @@ -877,7 +877,7 @@ xlink:href="#linearGradient3880" id="radialGradient3957" gradientUnits="userSpaceOnUse" - gradientTransform="matrix(1,0,0,1.0000234,0,-0.00345228)" + gradientTransform="matrix(0.90336573,0,0,0.90338687,22.936413,14.223841)" cx="237.35278" cy="147.22479" fx="237.35278" @@ -888,7 +888,7 @@ xlink:href="#linearGradient3846" id="radialGradient3959" gradientUnits="userSpaceOnUse" - gradientTransform="matrix(0.43641683,0,0,0.43645606,205.85223,33.676924)" + gradientTransform="matrix(0.40004875,0,0,0.40008471,208.47728,43.13925)" cx="72.191498" cy="260.15875" fx="72.191498" @@ -1003,84 +1003,18 @@ - - - - - - - - - - - - - - - - + transform="matrix(10.683152,0,0,10.683152,-3645.2454,641.29516)"> - - - From c56007cceb18662f090ee2f350bb2b69ec7c937c Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 23 Aug 2013 14:11:33 +0200 Subject: [PATCH 087/194] fixed a record counting bug (was using the wrong model) --- apps/opencs/view/world/table.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 75cbddc2a..72e78c738 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -257,9 +257,9 @@ void CSVWorld::Table::tableSizeUpdate() int deleted = 0; int modified = 0; - if (mModel->columnCount()>0) + if (mProxyModel->columnCount()>0) { - int rows = mModel->rowCount(); + int rows = mProxyModel->rowCount(); for (int i=0; i Date: Fri, 23 Aug 2013 14:49:41 +0200 Subject: [PATCH 088/194] fixed type in error message --- apps/opencs/model/filter/textnode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index ec654267b..9987c66d2 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -19,7 +19,7 @@ bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, const std::map::const_iterator iter = columns.find (mColumnId); if (iter==columns.end()) - throw std::logic_error ("invalid column in test node test"); + throw std::logic_error ("invalid column in text node test"); if (iter->second==-1) return true; From f5d03a16c1dede5e7ddc3a4f2af38917740d66b4 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Fri, 23 Aug 2013 07:01:30 -0700 Subject: [PATCH 089/194] Rename getFacedObject and getFacedHandle for melee hits --- apps/openmw/mwbase/world.hpp | 7 ++++--- apps/openmw/mwclass/npc.cpp | 3 ++- apps/openmw/mwworld/physicssystem.cpp | 5 ++++- apps/openmw/mwworld/physicssystem.hpp | 7 ++++--- apps/openmw/mwworld/worldimp.cpp | 9 +++++---- apps/openmw/mwworld/worldimp.hpp | 7 ++++--- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index aa5a38e6d..6ca900a4d 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -220,9 +220,10 @@ namespace MWBase virtual MWWorld::Ptr getFacedObject() = 0; ///< Return pointer to the object the player is looking at, if it is within activation range - /// Returns a pointer to the object the provided object is facing (if within the - /// specified distance). This will attempt to use the "Bip01 Head" node as a basis. - virtual MWWorld::Ptr getFacedObject(const MWWorld::Ptr &ptr, float distance) = 0; + /// Returns a pointer to the object the provided object would hit (if within the + /// specified distance), and the point where the hit occurs. This will attempt to + /// use the "Head" node as a basis. + virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance) = 0; virtual void adjustPosition (const MWWorld::Ptr& ptr) = 0; ///< Adjust position after load to be on ground. Must be called after model load. diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index d9c1da77b..073d1b1b9 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -332,7 +332,8 @@ namespace MWClass float dist = 100.0f * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : gmst.find("fHandToHandReach")->getFloat()); - MWWorld::Ptr victim = world->getFacedObject(ptr, dist); + // TODO: Use second to work out the hit angle and where to spawn the blood effect + MWWorld::Ptr victim = world->getHitContact(ptr, dist).first; if(victim.isEmpty()) // Didn't hit anything return; diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 33ba7101e..9fa8db782 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -311,7 +311,10 @@ namespace MWWorld return results; } - std::pair PhysicsSystem::getFacedHandle(const Ogre::Vector3 &origin_, const Ogre::Quaternion &orient_, float queryDistance) + std::pair PhysicsSystem::getHitContact(const std::string &name, + const Ogre::Vector3 &origin_, + const Ogre::Quaternion &orient_, + float queryDistance) { btVector3 origin(origin_.x, origin_.y, origin_.z); diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 669eb74bd..f76b4d29c 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -57,9 +57,10 @@ namespace MWWorld Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr); std::pair getFacedHandle (MWWorld::World& world, float queryDistance); - std::pair getFacedHandle(const Ogre::Vector3 &origin, - const Ogre::Quaternion &orientation, - float queryDistance); + std::pair getHitContact(const std::string &name, + const Ogre::Vector3 &origin, + const Ogre::Quaternion &orientation, + float queryDistance); std::vector < std::pair > getFacedHandles (float queryDistance); std::vector < std::pair > getFacedHandles (float mouseX, float mouseY, float queryDistance); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 950f00565..eee9c7a19 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -784,7 +784,7 @@ namespace MWWorld return object; } - MWWorld::Ptr World::getFacedObject(const MWWorld::Ptr &ptr, float distance) + std::pair World::getHitContact(const MWWorld::Ptr &ptr, float distance) { const ESM::Position &posdata = ptr.getRefData().getPosition(); Ogre::Vector3 pos(posdata.pos); @@ -799,11 +799,12 @@ namespace MWWorld pos += node->_getDerivedPosition(); } - std::pair result = mPhysics->getFacedHandle(pos, rot, distance); + std::pair result = mPhysics->getHitContact(ptr.getRefData().getHandle(), + pos, rot, distance); if(result.first.empty()) - return MWWorld::Ptr(); + return std::make_pair(MWWorld::Ptr(), Ogre::Vector3(0.0f)); - return searchPtrViaHandle(result.first); + return std::make_pair(searchPtrViaHandle(result.first), result.second); } void World::deleteObject (const Ptr& ptr) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b8a9a4e82..71391239b 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -252,9 +252,10 @@ namespace MWWorld virtual MWWorld::Ptr getFacedObject(); ///< Return pointer to the object the player is looking at, if it is within activation range - /// Returns a pointer to the object the provided object is facing (if within the - /// specified distance). This will attempt to use the "Bip01 Head" node as a basis. - virtual MWWorld::Ptr getFacedObject(const MWWorld::Ptr &ptr, float distance); + /// Returns a pointer to the object the provided object would hit (if within the + /// specified distance), and the point where the hit occurs. This will attempt to + /// use the "Head" node as a basis. + virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance); virtual void deleteObject (const Ptr& ptr); From 3fa65f21ddc3cdbb9e116eea5b61315944a7adf8 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Fri, 23 Aug 2013 12:25:57 -0700 Subject: [PATCH 090/194] Use a proper cone shape with a contact test to check for melee hits --- apps/openmw/mwworld/physicssystem.cpp | 43 ++++++----- libs/openengine/bullet/physic.cpp | 107 +++++++++++++++----------- libs/openengine/bullet/physic.hpp | 8 +- 3 files changed, 89 insertions(+), 69 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 9fa8db782..761e5111c 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -312,28 +312,31 @@ namespace MWWorld } std::pair PhysicsSystem::getHitContact(const std::string &name, - const Ogre::Vector3 &origin_, - const Ogre::Quaternion &orient_, + const Ogre::Vector3 &origin, + const Ogre::Quaternion &orient, float queryDistance) { - btVector3 origin(origin_.x, origin_.y, origin_.z); - - std::pair result = mEngine->sphereTest(queryDistance,origin); - if(result.first == "") return std::make_pair("",0); - btVector3 a = result.second - origin; - Ogre::Vector3 a_ = Ogre::Vector3(a.x(),a.y(),a.z()); - a_ = orient_.Inverse()*a_; - Ogre::Vector2 a_xy = Ogre::Vector2(a_.x,a_.y); - Ogre::Vector2 a_yz = Ogre::Vector2(a_xy.length(),a_.z); - float axy = a_xy.angleBetween(Ogre::Vector2::UNIT_Y).valueDegrees(); - float az = a_yz.angleBetween(Ogre::Vector2::UNIT_X).valueDegrees(); - - float fCombatAngleXY = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatAngleXY")->getFloat(); - float fCombatAngleZ = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatAngleZ")->getFloat(); - if(abs(axy) < fCombatAngleXY && abs(az) < fCombatAngleZ) - return std::make_pair (result.first,result.second.length()); - else - return std::make_pair("",0); + const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); + + btConeShape shape(Ogre::Degree(store.find("fCombatAngleXY")->getFloat()/2.0f).valueRadians(), + queryDistance); + shape.setLocalScaling(btVector3(1, 1, Ogre::Degree(store.find("fCombatAngleZ")->getFloat()/2.0f).valueRadians() / + shape.getRadius())); + + // The shape origin is its center, so we have to move it forward by half the length. The + // real origin will be provided to getFilteredContact to find the closest. + Ogre::Vector3 center = origin + (orient * Ogre::Vector3(0.0f, queryDistance*0.5f, 0.0f)); + + btCollisionObject object; + object.setCollisionShape(&shape); + object.setWorldTransform(btTransform(btQuaternion(orient.x, orient.y, orient.z, orient.w), + btVector3(center.x, center.y, center.z))); + + std::pair result = mEngine->getFilteredContact( + name, btVector3(origin.x, origin.y, origin.z), &object); + if(!result.first) + return std::make_pair(std::string(), Ogre::Vector3(&result.second[0])); + return std::make_pair(result.first->mName, Ogre::Vector3(&result.second[0])); } diff --git a/libs/openengine/bullet/physic.cpp b/libs/openengine/bullet/physic.cpp index e488d6e99..e33edda18 100644 --- a/libs/openengine/bullet/physic.cpp +++ b/libs/openengine/bullet/physic.cpp @@ -543,61 +543,63 @@ namespace Physic #endif }; - struct AabbResultCallback : public btBroadphaseAabbCallback { - std::vector hits; - //AabbResultCallback(){} - virtual bool process(const btBroadphaseProxy* proxy) { - RigidBody* collisionObject = static_cast(proxy->m_clientObject); - if(proxy->m_collisionFilterGroup == CollisionType_Actor && (collisionObject->mName != "player")) - this->hits.push_back(collisionObject); - return true; - } - }; - - - std::pair PhysicEngine::sphereTest(float radius,btVector3& pos) + class DeepestNotMeContactTestResultCallback : public btCollisionWorld::ContactResultCallback { - AabbResultCallback callback; - /*btDefaultMotionState* newMotionState = - new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1),pos)); - btCollisionShape * shape = new btSphereShape(radius); - btRigidBody::btRigidBodyConstructionInfo CI = btRigidBody::btRigidBodyConstructionInfo - (0,newMotionState, shape); - RigidBody* body = new RigidBody(CI,"hitDetectionShpere__"); - btTransform tr = body->getWorldTransform(); - tr.setOrigin(pos); - body->setWorldTransform(tr); - dynamicsWorld->addRigidBody(body,CollisionType_Actor,CollisionType_World|CollisionType_World); - body->setWorldTransform(tr);*/ + const std::string &mFilter; + // Store the real origin, since the shape's origin is its center + btVector3 mOrigin; + + public: + const RigidBody *mObject; + btVector3 mContactPoint; + btScalar mLeastDistSqr; - btVector3 aabbMin = pos - radius*btVector3(1.0f, 1.0f, 1.0f); - btVector3 aabbMax = pos + radius*btVector3(1.0f, 1.0f, 1.0f); + DeepestNotMeContactTestResultCallback(const std::string &filter, const btVector3 &origin) + : mFilter(filter), mOrigin(origin), mObject(0), mContactPoint(0,0,0), + mLeastDistSqr(std::numeric_limits::max()) + { } - broadphase->aabbTest(aabbMin,aabbMax,callback); - for(int i=0;i (callback.hits.size()); ++i) +#if defined(BT_COLLISION_OBJECT_WRAPPER_H) + virtual btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, + const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) { - float d = (callback.hits[i]->getWorldTransform().getOrigin()-pos).length(); - if(d(col1Wrap->m_collisionObject); + if(body && body->mName != mFilter) { - std::pair rayResult = this->rayTest(pos,callback.hits[i]->getWorldTransform().getOrigin()); - if(rayResult.second>d || rayResult.first == callback.hits[i]->mName) - return std::make_pair(callback.hits[i]->mName,callback.hits[i]->getWorldTransform().getOrigin()); + btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); + if(!mObject || distsqr < mLeastDistSqr) + { + mObject = body; + mLeastDistSqr = distsqr; + mContactPoint = cp.getPositionWorldOnA(); + } } + + return 0.f; } - //ContactTestResultCallback callback; - //dynamicsWorld->contactTest(body, callback); - //dynamicsWorld->removeRigidBody(body); - //delete body; - //delete shape; - //if(callback.mResultName.empty()) return std::make_pair(std::string(""),btVector3(0,0,0)); - /*for(int i=0;i(callback.mResultName[i],callback.mResultContact[i]); - */ - return std::make_pair(std::string(""),btVector3(0,0,0)); - } + const RigidBody* body = dynamic_cast(col1); + if(body && body->mName != mFilter) + { + btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); + if(!mObject || distsqr < mLeastDistSqr) + { + mObject = body; + mLeastDistSqr = distsqr; + mContactPoint = cp.getPositionWorldOnA(); + } + } + + return 0.f; + } +#endif + }; + std::vector PhysicEngine::getCollisions(const std::string& name) { @@ -607,6 +609,17 @@ namespace Physic return callback.mResult; } + + std::pair PhysicEngine::getFilteredContact(const std::string &filter, + const btVector3 &origin, + btCollisionObject *object) + { + DeepestNotMeContactTestResultCallback callback(filter, origin); + dynamicsWorld->contactTest(object, callback); + return std::make_pair(callback.mObject, callback.mContactPoint); + } + + void PhysicEngine::stepSimulation(double deltaT) { // This seems to be needed for character controller objects diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index 46dafda76..f28f95ccb 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -321,10 +321,14 @@ public: std::pair sphereCast (float radius, btVector3& from, btVector3& to); ///< @return (hit, relative distance) - std::pair sphereTest(float radius,btVector3& pos); - std::vector getCollisions(const std::string& name); + // Get the nearest object that's inside the given object, filtering out objects of the + // provided name + std::pair getFilteredContact(const std::string &filter, + const btVector3 &origin, + btCollisionObject *object); + //event list of non player object std::list NPEventList; From ca24a809fc416bc3b5566e4cfd790e3e9c109a67 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Fri, 23 Aug 2013 13:38:26 -0700 Subject: [PATCH 091/194] Use the position of the actor to determine if they're swimming --- apps/openmw/mwworld/physicssystem.cpp | 35 ++++++++++++--------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 761e5111c..97aa7dffe 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -107,8 +107,7 @@ namespace MWWorld } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool isSwimming, bool isFlying, float waterlevel, - OEngine::Physic::PhysicEngine *engine) + bool isFlying, float waterlevel, OEngine::Physic::PhysicEngine *engine) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); @@ -135,11 +134,11 @@ namespace MWWorld bool isOnGround = false; Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; - if(isSwimming || isFlying) + if(position.z < waterlevel || isFlying) { - velocity = (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* - Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* - Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * + velocity = (Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)* + Ogre::Quaternion(Ogre::Radian(-refpos.rot[1]), Ogre::Vector3::UNIT_Y)* + Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * movement; } else @@ -173,7 +172,7 @@ namespace MWWorld { Ogre::Vector3 nextpos = newPosition + velocity*remainingTime; - if(isSwimming && !isFlying && + if(newPosition.z < waterlevel && !isFlying && nextpos.z > waterlevel && newPosition.z <= waterlevel) { const Ogre::Vector3 down(0,0,-1); @@ -197,7 +196,7 @@ namespace MWWorld // We hit something. Try to step up onto it. if(stepMove(colobj, newPosition, velocity, remainingTime, engine)) - isOnGround = !(isSwimming || isFlying); // Only on the ground if there's gravity + isOnGround = !(newPosition.z < waterlevel || isFlying); // Only on the ground if there's gravity else { // Can't move this way, try to find another spot along the plane @@ -208,7 +207,7 @@ namespace MWWorld // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. - if(!(isSwimming || isFlying)) + if(!(newPosition.z < waterlevel || isFlying)) velocity.z = std::min(velocity.z, 0.0f); } } @@ -225,7 +224,7 @@ namespace MWWorld isOnGround = false; } - if(isOnGround || isSwimming || isFlying) + if(isOnGround || newPosition.z < waterlevel || isFlying) physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { @@ -590,15 +589,13 @@ namespace MWWorld for(;iter != mMovementQueue.end();iter++) { float waterlevel = -std::numeric_limits::max(); - const MWWorld::CellStore *cellstore = iter->first.getCell(); - if(cellstore->mCell->hasWater()) - waterlevel = cellstore->mCell->mWater; - - Ogre::Vector3 newpos; - newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, - world->isSwimming(iter->first), - world->isFlying(iter->first), - waterlevel, mEngine); + const ESM::Cell *cell = iter->first.getCell()->mCell; + if(cell->hasWater()) + waterlevel = cell->mWater; + + Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, + world->isFlying(iter->first), + waterlevel, mEngine); mMovementResults.push_back(std::make_pair(iter->first, newpos)); } From 3cf60da5a70a8b7fa294756dc0fbb982e21ef5c9 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 14:43:38 +0200 Subject: [PATCH 092/194] added numeric value filter node --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/filter/parser.cpp | 127 ++++++++++++++++++++++++- apps/opencs/model/filter/parser.hpp | 2 + apps/opencs/model/filter/valuenode.cpp | 71 ++++++++++++++ apps/opencs/model/filter/valuenode.hpp | 37 +++++++ 5 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 apps/opencs/model/filter/valuenode.cpp create mode 100644 apps/opencs/model/filter/valuenode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index ea78135b9..ad7b5b19c 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -108,7 +108,7 @@ opencs_units_noqt (model/settings ) opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode + node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) opencs_hdrs_noqt (model/filter diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 7c8f9d768..5e580b6e1 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -14,6 +14,7 @@ #include "andnode.hpp" #include "notnode.hpp" #include "textnode.hpp" +#include "valuenode.hpp" namespace CSMFilter { @@ -35,7 +36,8 @@ namespace CSMFilter Type_Keyword_And, Type_Keyword_Or, Type_Keyword_Not, - Type_Keyword_Text + Type_Keyword_Text, + Type_Keyword_Value }; Type mType; @@ -169,7 +171,7 @@ CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) { "true", "false", "and", "or", "not", - "text", + "text", "value", 0 }; @@ -251,6 +253,10 @@ boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty) return parseText(); + case Token::Type_Keyword_Value: + + return parseValue(); + case Token::Type_EOS: if (!allowEmpty) @@ -378,6 +384,123 @@ boost::shared_ptr CSMFilter::Parser::parseText() return boost::shared_ptr (new TextNode (columnId, text)); } +boost::shared_ptr CSMFilter::Parser::parseValue() +{ + Token token = getNextToken(); + + if (token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (!token) + return boost::shared_ptr(); + + // parse column ID + int columnId = -1; + + if (token.mType==Token::Type_Number) + { + if (static_cast (token.mNumber)==token.mNumber) + columnId = static_cast (token.mNumber); + } + else if (token.mType==Token::Type_String) + { + columnId = CSMWorld::Columns::getId (token.mString); + } + + if (columnId<0) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + // parse value + double lower = 0; + double upper = 0; + bool min = false; + bool max = false; + + token = getNextToken(); + + if (token.mType==Token::Type_Number) + { + // single value + min = max = true; + lower = upper = token.mNumber; + } + else + { + // interval + if (token.mType==Token::Type_OpenSquare) + min = true; + else if (token.mType!=Token::Type_CloseSquare && token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Number) + { + error(); + return boost::shared_ptr(); + } + + lower = token.mNumber; + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Number) + { + error(); + return boost::shared_ptr(); + } + + upper = token.mNumber; + + token = getNextToken(); + + if (token.mType==Token::Type_CloseSquare) + max = true; + else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + + return boost::shared_ptr (new ValueNode (columnId, lower, upper, min, max)); +} + void CSMFilter::Parser::error() { mError = true; diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 2512de141..1600992b7 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -32,6 +32,8 @@ namespace CSMFilter boost::shared_ptr parseText(); + boost::shared_ptr parseValue(); + void error(); public: diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp new file mode 100644 index 000000000..f6cb20e4c --- /dev/null +++ b/apps/opencs/model/filter/valuenode.cpp @@ -0,0 +1,71 @@ + +#include "valuenode.hpp" + +#include +#include + +#include "../world/columns.hpp" +#include "../world/idtable.hpp" + +CSMFilter::ValueNode::ValueNode (int columnId, + double lower, double upper, bool min, bool max) +: mColumnId (columnId), mLower (lower), mUpper (upper), mMin (min), mMax (max) +{} + +bool CSMFilter::ValueNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + const std::map::const_iterator iter = columns.find (mColumnId); + + if (iter==columns.end()) + throw std::logic_error ("invalid column in test value test"); + + if (iter->second==-1) + return true; + + QModelIndex index = table.index (row, iter->second); + + QVariant data = table.data (index); + + if (data.type()!=QVariant::Double && data.type()!=QVariant::Bool && data.type()!=QVariant::Int && + data.type()!=QVariant::UInt) + return false; + + double value = data.toDouble(); + + if (mLower==mUpper && mMin && mMax) + return value==mLower; + + return (mMin ? value>=mLower : value>mLower) && (mMax ? value<=mUpper : value CSMFilter::ValueNode::getReferencedColumns() const +{ + return std::vector (1, mColumnId); +} + +std::string CSMFilter::ValueNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << "value ("; + + if (numericColumns) + stream << mColumnId; + else + stream + << "\"" + << CSMWorld::Columns::getName (static_cast (mColumnId)) + << "\""; + + stream << ", \""; + + if (mLower==mUpper && mMin && mMax) + stream << mLower; + else + stream << (mMin ? "[" : "(") << mLower << ", " << mUpper << (mMax ? "]" : ")"); + + stream << ")"; + + return stream.str(); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/valuenode.hpp b/apps/opencs/model/filter/valuenode.hpp new file mode 100644 index 000000000..faaa1e2ff --- /dev/null +++ b/apps/opencs/model/filter/valuenode.hpp @@ -0,0 +1,37 @@ +#ifndef CSM_FILTER_VALUENODE_H +#define CSM_FILTER_VALUENODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class ValueNode : public LeafNode + { + int mColumnId; + std::string mText; + double mLower; + double mUpper; + bool mMin; + bool mMax; + + public: + + ValueNode (int columnId, double lower, double upper, bool min, bool max); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif From cf58670c85619ab33745a6c7f28e6e9228431919 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 14:46:42 +0200 Subject: [PATCH 093/194] removed global filter scope (would have caused sync problems between projects) --- apps/opencs/model/filter/filter.hpp | 7 +++---- apps/opencs/view/filter/filtercreator.cpp | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/opencs/model/filter/filter.hpp b/apps/opencs/model/filter/filter.hpp index 7630ad410..62170ca80 100644 --- a/apps/opencs/model/filter/filter.hpp +++ b/apps/opencs/model/filter/filter.hpp @@ -13,10 +13,9 @@ namespace CSMFilter { enum Scope { - Scope_Global = 0, // per user - Scope_Project = 1, // per project - Scope_Session = 2, // exists only for one editing session; not saved - Scope_Content = 3 // embedded in the edited content file + Scope_Project = 0, // per project + Scope_Session = 1, // exists only for one editing session; not saved + Scope_Content = 2 // embedded in the edited content file }; Scope mScope; diff --git a/apps/opencs/view/filter/filtercreator.cpp b/apps/opencs/view/filter/filtercreator.cpp index 476eacbe1..47925ea57 100644 --- a/apps/opencs/view/filter/filtercreator.cpp +++ b/apps/opencs/view/filter/filtercreator.cpp @@ -10,7 +10,6 @@ std::string CSVFilter::FilterCreator::getNamespace() const { switch (mScope->currentIndex()) { - case CSMFilter::Filter::Scope_Global: return "global::"; case CSMFilter::Filter::Scope_Project: return "project::"; case CSMFilter::Filter::Scope_Session: return "session::"; } @@ -38,7 +37,6 @@ CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoS mScope = new QComboBox (this); - mScope->addItem ("Global"); mScope->addItem ("Project"); mScope->addItem ("Session"); /// \ŧodo re-enable for OpenMW 1.1 @@ -51,7 +49,7 @@ CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoS QLabel *label = new QLabel ("Scope", this); insertAtBeginning (label, false); - mScope->setCurrentIndex (2); + mScope->setCurrentIndex (1); } void CSVFilter::FilterCreator::reset() From 78c7de440d94492490e260777b28030377c64e56 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 14:49:05 +0200 Subject: [PATCH 094/194] simplified filter nodes even more --- apps/opencs/model/filter/leafnode.cpp | 4 ---- apps/opencs/model/filter/leafnode.hpp | 3 --- apps/opencs/model/filter/narynode.cpp | 4 ---- apps/opencs/model/filter/narynode.hpp | 4 ---- apps/opencs/model/filter/node.hpp | 3 --- apps/opencs/model/filter/unarynode.cpp | 5 ----- apps/opencs/model/filter/unarynode.hpp | 3 --- 7 files changed, 26 deletions(-) diff --git a/apps/opencs/model/filter/leafnode.cpp b/apps/opencs/model/filter/leafnode.cpp index a1330f9dc..055a1747c 100644 --- a/apps/opencs/model/filter/leafnode.cpp +++ b/apps/opencs/model/filter/leafnode.cpp @@ -6,7 +6,3 @@ std::vector CSMFilter::LeafNode::getReferencedColumns() const return std::vector(); } -bool CSMFilter::LeafNode::isSimple() const -{ - return true; -} diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp index a9790d01d..2f3d91070 100644 --- a/apps/opencs/model/filter/leafnode.hpp +++ b/apps/opencs/model/filter/leafnode.hpp @@ -14,9 +14,6 @@ namespace CSMFilter virtual std::vector getReferencedColumns() const; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. - - virtual bool isSimple() const; - ///< \return Can this filter be displayed in simple mode. }; } diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp index b96e7d4b2..98f706c87 100644 --- a/apps/opencs/model/filter/narynode.cpp +++ b/apps/opencs/model/filter/narynode.cpp @@ -57,8 +57,4 @@ std::string CSMFilter::NAryNode::toString (bool numericColumns) const return stream.str(); } -bool CSMFilter::NAryNode::isSimple() const -{ - return false; -} diff --git a/apps/opencs/model/filter/narynode.hpp b/apps/opencs/model/filter/narynode.hpp index 421847fae..aa501d009 100644 --- a/apps/opencs/model/filter/narynode.hpp +++ b/apps/opencs/model/filter/narynode.hpp @@ -31,10 +31,6 @@ namespace CSMFilter ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. - - virtual bool isSimple() const; - ///< \return Can this filter be displayed in simple mode. - }; } diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp index 6783c3b5e..ef18353a4 100644 --- a/apps/opencs/model/filter/node.hpp +++ b/apps/opencs/model/filter/node.hpp @@ -41,9 +41,6 @@ namespace CSMFilter ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. - virtual bool isSimple() const = 0; - ///< \return Can this filter be displayed in simple mode. - virtual std::string toString (bool numericColumns) const = 0; ///< Return a string that represents this node. /// diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp index d1897f4b7..43a24b76a 100644 --- a/apps/opencs/model/filter/unarynode.cpp +++ b/apps/opencs/model/filter/unarynode.cpp @@ -20,11 +20,6 @@ std::vector CSMFilter::UnaryNode::getReferencedColumns() const return mChild->getReferencedColumns(); } -bool CSMFilter::UnaryNode::isSimple() const -{ - return false; -} - std::string CSMFilter::UnaryNode::toString (bool numericColumns) const { return mName + " " + mChild->toString (numericColumns); diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp index 9fd5faf3f..6bbc96092 100644 --- a/apps/opencs/model/filter/unarynode.hpp +++ b/apps/opencs/model/filter/unarynode.hpp @@ -24,9 +24,6 @@ namespace CSMFilter ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. - virtual bool isSimple() const; - ///< \return Can this filter be displayed in simple mode. - virtual std::string toString (bool numericColumns) const; ///< Return a string that represents this node. /// From 51fbb0f3f46b3292849bddb2686f14b669248142 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 15:33:46 +0200 Subject: [PATCH 095/194] fixed a segfault when opening views for tables that do not allow the creation of new records --- apps/opencs/view/world/tablebottombox.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index 6cf21a132..3edf9af31 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -63,12 +63,15 @@ CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFacto mCreator = creatorFactory.makeCreator (data, undoStack, id); - mLayout->addWidget (mCreator); + if (mCreator) + { + mLayout->addWidget (mCreator); - connect (mCreator, SIGNAL (done()), this, SLOT (createRequestDone())); + connect (mCreator, SIGNAL (done()), this, SLOT (createRequestDone())); - connect (mCreator, SIGNAL (requestFocus (const std::string&)), - this, SIGNAL (requestFocus (const std::string&))); + connect (mCreator, SIGNAL (requestFocus (const std::string&)), + this, SIGNAL (requestFocus (const std::string&))); + } } void CSVWorld::TableBottomBox::setEditLock (bool locked) From 7aee1da5c6453d0c9a68eec3dbd56c2a4695eba8 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 16:51:49 +0200 Subject: [PATCH 096/194] fixed some columns --- apps/opencs/model/world/columnimp.hpp | 6 +++--- apps/opencs/model/world/columns.cpp | 9 +++++---- apps/opencs/model/world/columns.hpp | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 8a1595a30..6c31fddf3 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -257,7 +257,7 @@ namespace CSMWorld int mIndex; UseValueColumn (int index) - : Column (Columns::ColumnId_UseValue1 + index - 1, ColumnBase::Display_Float), + : Column (Columns::ColumnId_UseValue1 + index, ColumnBase::Display_Float), mIndex (index) {} @@ -339,7 +339,7 @@ namespace CSMWorld int mIndex; AttributesColumn (int index) - : Column (Columns::ColumnId_Attribute1 + index - 1, ColumnBase::Display_Attribute), + : Column (Columns::ColumnId_Attribute1 + index, ColumnBase::Display_Attribute), mIndex (index) {} @@ -372,7 +372,7 @@ namespace CSMWorld SkillsColumn (int index, bool typePrefix = false, bool major = false) : Column ((typePrefix ? ( major ? Columns::ColumnId_MajorSkill1 : Columns::ColumnId_MinorSkill1) : - Columns::ColumnId_Skill1) + index - 1, ColumnBase::Display_String), + Columns::ColumnId_Skill1) + index, ColumnBase::Display_String), mIndex (index), mMajor (major) {} diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 2198d1b0c..f6eb8fe34 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -166,10 +166,11 @@ namespace CSMWorld { ColumnId_MinorSkill5, "Minor Skill 5" }, { ColumnId_Skill1, "Skill 1" }, - { ColumnId_Skill1, "Skill 2" }, - { ColumnId_Skill1, "Skill 3" }, - { ColumnId_Skill1, "Skill 4" }, - { ColumnId_Skill1, "Skill 5" }, + { ColumnId_Skill2, "Skill 2" }, + { ColumnId_Skill3, "Skill 3" }, + { ColumnId_Skill4, "Skill 4" }, + { ColumnId_Skill5, "Skill 5" }, + { ColumnId_Skill6, "Skill 6" }, { -1, 0 } // end marker }; diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index adde80dc9..28da60e93 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -171,7 +171,8 @@ namespace CSMWorld ColumnId_Skill2 = 0x50001, ColumnId_Skill3 = 0x50002, ColumnId_Skill4 = 0x50003, - ColumnId_Skill5 = 0x50004 + ColumnId_Skill5 = 0x50004, + ColumnId_Skill6 = 0x50005 }; std::string getName (ColumnId column); From d1516792ce28001bf1a316382e10dc9603e2aab2 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 17:17:22 +0200 Subject: [PATCH 097/194] made document data available to filter parser --- apps/opencs/model/filter/parser.cpp | 3 ++- apps/opencs/model/filter/parser.hpp | 8 +++++++- apps/opencs/view/filter/editwidget.cpp | 4 ++-- apps/opencs/view/filter/editwidget.hpp | 7 ++++++- apps/opencs/view/filter/filterbox.cpp | 4 ++-- apps/opencs/view/filter/filterbox.hpp | 7 ++++++- apps/opencs/view/filter/recordfilterbox.cpp | 4 ++-- apps/opencs/view/filter/recordfilterbox.hpp | 7 ++++++- apps/opencs/view/world/tablesubview.cpp | 2 +- 9 files changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 5e580b6e1..2cf882f2d 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -506,7 +506,8 @@ void CSMFilter::Parser::error() mError = true; } -CSMFilter::Parser::Parser() : mIndex (0), mError (false) {} +CSMFilter::Parser::Parser (const CSMWorld::Data& data) +: mIndex (0), mError (false), mData (data) {} bool CSMFilter::Parser::parse (const std::string& filter) { diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 1600992b7..c26168611 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -5,6 +5,11 @@ #include "node.hpp" +namespace CSMWorld +{ + class Data; +} + namespace CSMFilter { struct Token; @@ -15,6 +20,7 @@ namespace CSMFilter std::string mInput; int mIndex; bool mError; + const CSMWorld::Data& mData; Token getStringToken(); @@ -38,7 +44,7 @@ namespace CSMFilter public: - Parser(); + Parser (const CSMWorld::Data& data); bool parse (const std::string& filter); ///< Discards any previous calls to parse diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index b691a5e16..98d23efdb 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -1,8 +1,8 @@ #include "editwidget.hpp" -CSVFilter::EditWidget::EditWidget (QWidget *parent) -: QLineEdit (parent) +CSVFilter::EditWidget::EditWidget (const CSMWorld::Data& data, QWidget *parent) +: QLineEdit (parent), mParser (data) { mPalette = palette(); connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index 76b484de9..72b2659d5 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -9,6 +9,11 @@ #include "../../model/filter/parser.hpp" #include "../../model/filter/node.hpp" +namespace CSMWorld +{ + class Data; +} + namespace CSVFilter { class EditWidget : public QLineEdit @@ -20,7 +25,7 @@ namespace CSVFilter public: - EditWidget (QWidget *parent = 0); + EditWidget (const CSMWorld::Data& data, QWidget *parent = 0); signals: diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index 495abf871..af449437b 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -5,14 +5,14 @@ #include "recordfilterbox.hpp" -CSVFilter::FilterBox::FilterBox (QWidget *parent) +CSVFilter::FilterBox::FilterBox (const CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); layout->setContentsMargins (0, 0, 0, 0); - RecordFilterBox *recordFilterBox = new RecordFilterBox (this); + RecordFilterBox *recordFilterBox = new RecordFilterBox (data, this); layout->addWidget (recordFilterBox); diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index d3806876d..60d2f038f 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -5,6 +5,11 @@ #include "../../model/filter/node.hpp" +namespace CSMWorld +{ + class Data; +} + namespace CSVFilter { class FilterBox : public QWidget @@ -13,7 +18,7 @@ namespace CSVFilter public: - FilterBox (QWidget *parent = 0); + FilterBox (const CSMWorld::Data& data, QWidget *parent = 0); signals: diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index 3b5f73f47..6b1831e30 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -6,7 +6,7 @@ #include "editwidget.hpp" -CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) +CSVFilter::RecordFilterBox::RecordFilterBox (const CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); @@ -15,7 +15,7 @@ CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) layout->addWidget (new QLabel ("Record Filter", this)); - EditWidget *editWidget = new EditWidget (this); + EditWidget *editWidget = new EditWidget (data, this); layout->addWidget (editWidget); diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 64c1848a8..6b1b0e9bb 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -9,6 +9,11 @@ #include "../../model/filter/node.hpp" +namespace CSMWorld +{ + class Data; +} + namespace CSVFilter { class RecordFilterBox : public QWidget @@ -17,7 +22,7 @@ namespace CSVFilter public: - RecordFilterBox (QWidget *parent = 0); + RecordFilterBox (const CSMWorld::Data& data, QWidget *parent = 0); signals: diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index a43ae2dac..1e05fbf51 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -25,7 +25,7 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D layout->insertWidget (0, mTable = new Table (id, document.getData(), document.getUndoStack(), mBottom->canCreateAndDelete()), 2); - CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (this); + CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (document.getData(), this); layout->insertWidget (0, filterBox); From 25e6380884f4b9e9495516506c90975a68910e9a Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 17:40:00 +0200 Subject: [PATCH 098/194] implemented use of predefined filters --- apps/opencs/model/filter/parser.cpp | 67 +++++++++++++++++++++++------ apps/opencs/model/filter/parser.hpp | 2 +- apps/opencs/model/world/data.cpp | 10 +++++ apps/opencs/model/world/data.hpp | 4 ++ 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 2cf882f2d..b800faa1e 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -8,6 +8,8 @@ #include #include "../world/columns.hpp" +#include "../world/data.hpp" +#include "../world/idcollection.hpp" #include "booleannode.hpp" #include "ornode.hpp" @@ -31,6 +33,7 @@ namespace CSMFilter Type_OpenSquare, Type_CloseSquare, Type_Comma, + Type_OneShot, Type_Keyword_True, ///< \attention Keyword enums must be arranged continuously. Type_Keyword_False, Type_Keyword_And, @@ -208,6 +211,7 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() case '[': ++mIndex; return Token (Token::Type_OpenSquare); case ']': ++mIndex; return Token (Token::Type_CloseSquare); case ',': ++mIndex; return Token (Token::Type_Comma); + case '?': ++mIndex; return Token (Token::Type_OneShot); } if (c=='"' || c=='_' || std::isalpha (c)) @@ -509,7 +513,7 @@ void CSMFilter::Parser::error() CSMFilter::Parser::Parser (const CSMWorld::Data& data) : mIndex (0), mError (false), mData (data) {} -bool CSMFilter::Parser::parse (const std::string& filter) +bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) { // reset mFilter.reset(); @@ -517,23 +521,62 @@ bool CSMFilter::Parser::parse (const std::string& filter) mInput = filter; mIndex = 0; - boost::shared_ptr node = parseImp (true); + Token token = getNextToken(); - if (mError) - return false; + if (token==Token (Token::Type_OneShot)) + { + boost::shared_ptr node = parseImp (true); - if (getNextToken()!=Token (Token::Type_EOS)) - return false; + if (mError) + return false; + + if (getNextToken()!=Token (Token::Type_EOS)) + { + error(); + return false; + } + + if (node) + mFilter = node; + else + { + // Empty filter string equals to filter "true". + mFilter.reset (new BooleanNode (true)); + } - if (node) - mFilter = node; + return true; + } + else if (token.mType==Token::Type_String && allowPredefined) + { + if (getNextToken()!=Token (Token::Type_EOS)) + { + error(); + return false; + } + + int index = mData.getFilters().searchId (token.mString); + + if (index==-1) + { + error(); + return false; + } + + const CSMWorld::Record& record = mData.getFilters().getRecord (index); + + if (record.isDeleted()) + { + error(); + return false; + } + + return parse (record.get().mFilter, false); + } else { - // Empty filter string equals to filter "true". - mFilter.reset (new BooleanNode (true)); + error(); + return false; } - - return true; } boost::shared_ptr CSMFilter::Parser::getFilter() const diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index c26168611..fbaf6972e 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -46,7 +46,7 @@ namespace CSMFilter Parser (const CSMWorld::Data& data); - bool parse (const std::string& filter); + bool parse (const std::string& filter, bool allowPredefined = true); ///< Discards any previous calls to parse /// /// \return Success? diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 4a5dcb38f..b9f6d1718 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -316,6 +316,16 @@ CSMWorld::RefCollection& CSMWorld::Data::getReferences() return mRefs; } +const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const +{ + return mFilters; +} + +CSMWorld::IdCollection& CSMWorld::Data::getFilters() +{ + return mFilters; +} + QAbstractItemModel *CSMWorld::Data::getTableModel (const UniversalId& id) { std::map::iterator iter = mModelIndex.find (id.getType()); diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index aebdd6ecd..2f8a2117e 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -119,6 +119,10 @@ namespace CSMWorld RefCollection& getReferences(); + const IdCollection& getFilters() const; + + IdCollection& getFilters(); + QAbstractItemModel *getTableModel (const UniversalId& id); ///< If no table model is available for \a id, an exception is thrown. /// From 7e02c9acf229daad0a17f03c113dfda1c1b479ab Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 18:53:11 +0200 Subject: [PATCH 099/194] added filter text column to filter table --- apps/opencs/model/world/columnimp.hpp | 25 +++++++++++++++++++++++++ apps/opencs/model/world/columns.cpp | 1 + apps/opencs/model/world/columns.hpp | 1 + apps/opencs/model/world/data.cpp | 1 + 4 files changed, 28 insertions(+) diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 6c31fddf3..1a2bf9df1 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -1191,6 +1191,31 @@ namespace CSMWorld return true; } }; + + template + struct FilterColumn : public Column + { + FilterColumn() : Column (Columns::ColumnId_Filter, ColumnBase::Display_String) {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mFilter.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mFilter = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; } #endif diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index f6eb8fe34..b20632258 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -144,6 +144,7 @@ namespace CSMWorld { ColumnId_MaxThrust, "Max Thrust" }, { ColumnId_Magical, "Magical" }, { ColumnId_Silver, "Silver" }, + { ColumnId_Filter, "Filter" }, { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 28da60e93..9a39e1678 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -138,6 +138,7 @@ namespace CSMWorld ColumnId_MaxThrust = 106, ColumnId_Magical = 107, ColumnId_Silver = 108, + ColumnId_Filter = 109, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index b9f6d1718..fbdbb4413 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -150,6 +150,7 @@ CSMWorld::Data::Data() : mRefs (mCells) mFilters.addColumn (new StringIdColumn); mFilters.addColumn (new RecordStateColumn); + mFilters.addColumn (new FilterColumn); mFilters.addColumn (new DescriptionColumn); addModel (new IdTable (&mGlobals), UniversalId::Type_Globals, UniversalId::Type_Global); From d007d4dc9a921bcc592790182b54edb58278a909 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 24 Aug 2013 19:12:47 +0200 Subject: [PATCH 100/194] allow colons in names (filter) --- apps/opencs/model/filter/parser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index b800faa1e..2320aa0be 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -92,7 +92,7 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() { char c = mInput[mIndex]; - if (std::isalpha (c) || c=='_' || (!string.empty() && std::isdigit (c)) || c=='"' || + if (std::isalpha (c) || c==':' || c=='_' || (!string.empty() && std::isdigit (c)) || c=='"' || (!string.empty() && string[0]=='"')) string += c; else @@ -214,7 +214,7 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() case '?': ++mIndex; return Token (Token::Type_OneShot); } - if (c=='"' || c=='_' || std::isalpha (c)) + if (c=='"' || c=='_' || std::isalpha (c) || c==':') return getStringToken(); if (c=='-' || c=='.' || std::isdigit (c)) From abd03245d783a3001842d109398e990c9946181d Mon Sep 17 00:00:00 2001 From: Marek Kochanowicz Date: Sat, 24 Aug 2013 19:17:44 +0200 Subject: [PATCH 101/194] Adding .directory to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 776e2b659..8aa02070b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ data CMakeLists.txt.user *.swp *.swo +.directory From c57fb4fc4fae7e0d9e505bfc08d0b87e2442cfc0 Mon Sep 17 00:00:00 2001 From: sirherrbatka Date: Sat, 24 Aug 2013 19:20:12 +0200 Subject: [PATCH 102/194] Delete .directory --- files/opencs/raster/.directory | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 files/opencs/raster/.directory diff --git a/files/opencs/raster/.directory b/files/opencs/raster/.directory deleted file mode 100644 index a68c37e30..000000000 --- a/files/opencs/raster/.directory +++ /dev/null @@ -1,8 +0,0 @@ -[Dolphin] -GroupedSorting=true -PreviewsShown=true -SortFoldersFirst=false -Timestamp=2013,8,23,12,44,33 -Version=3 -ViewMode=2 -VisibleRoles=Compact_text,Compact_size From 8998b90e8b20804804f753883a468b6c87c20298 Mon Sep 17 00:00:00 2001 From: mckibbenta Date: Sat, 24 Aug 2013 21:19:12 -0400 Subject: [PATCH 103/194] initial do-nothing implementation; registered opcode --- apps/openmw/mwbase/world.hpp | 3 +++ apps/openmw/mwscript/docs/vmformat.txt | 3 ++- apps/openmw/mwscript/miscextensions.cpp | 14 ++++++++++++++ apps/openmw/mwscript/statsextensions.cpp | 3 +-- apps/openmw/mwworld/worldimp.cpp | 7 ++++++- apps/openmw/mwworld/worldimp.hpp | 2 ++ components/compiler/extensions0.cpp | 2 ++ components/compiler/opcodes.hpp | 1 + 8 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index aa5a38e6d..147410c15 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -394,6 +394,9 @@ namespace MWBase /// Sets the NPC's Acrobatics skill to match the fWerewolfAcrobatics GMST. /// It only applies to the current form the NPC is in. virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) = 0; + + // Dummy Implementation + virtual bool toggleGodMode() = 0; }; } diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index 08b499175..ff3b60ca6 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -351,5 +351,6 @@ op 0x200021b: SetWerewolfAcrobatics op 0x200021c: SetWerewolfAcrobaticsExplicit op 0x200021d: ShowVars op 0x200021e: ShowVarsExplicit +op 0x200021f: ToggleGodMode -opcodes 0x200021f-0x3ffffff unused +opcodes 0x2000220-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 3141f13a2..a8d8a5f2b 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -717,6 +717,19 @@ namespace MWScript } }; + class OpToggleGodMode : public Interpreter::Opcode0 + { + public: + virtual void execute (Interpreter::Runtime& runtime) + { + InterpreterContext& context = static_cast (runtime.getContext()); + + bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); + + context.report (enabled ? "God Mode -> On" : "God Mode -> Off"); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { @@ -775,6 +788,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeEnableTeleporting, new OpEnableTeleporting); interpreter.installSegment5 (Compiler::Misc::opcodeShowVars, new OpShowVars); interpreter.installSegment5 (Compiler::Misc::opcodeShowVarsExplicit, new OpShowVars); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleGodMode, new OpToggleGodMode); } } } diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index eca9d279b..603515ff4 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -1081,7 +1081,6 @@ namespace MWScript } }; - void installOpcodes (Interpreter::Interpreter& interpreter) { for (int i=0; i); interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolfExplicit, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobatics, new OpSetWerewolfAcrobatics); - interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); + interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); } } } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 950f00565..73fa3f620 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1154,7 +1154,7 @@ namespace MWWorld bool World::toggleCollisionMode() { - return mPhysics->toggleCollisionMode();; + return mPhysics->toggleCollisionMode(); } bool World::toggleRenderMode (RenderMode mode) @@ -1942,4 +1942,9 @@ namespace MWWorld stats.getSkill(ESM::Skill::Acrobatics).setModified(gmst.find("fWerewolfAcrobatics")->getFloat(), 0); } + bool World::toggleGodMode() + { + return false; + } + } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b8a9a4e82..3c13c33c1 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -438,6 +438,8 @@ namespace MWWorld virtual void setWerewolf(const MWWorld::Ptr& actor, bool werewolf); virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor); + + virtual bool toggleGodMode(); }; } diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 9e0c36825..415f8d168 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -258,6 +258,8 @@ namespace Compiler extensions.registerInstruction ("enableteleporting", "", opcodeEnableTeleporting); extensions.registerInstruction ("showvars", "", opcodeShowVars, opcodeShowVarsExplicit); extensions.registerInstruction ("sv", "", opcodeShowVars, opcodeShowVarsExplicit); + extensions.registerInstruction("tgm", "", opcodeToggleGodMode); + extensions.registerInstruction("togglegodmode", "", opcodeToggleGodMode); } } diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index c4e2c1bc6..5eb54208a 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -221,6 +221,7 @@ namespace Compiler const int opcodeEnableTeleporting = 0x2000216; const int opcodeShowVars = 0x200021d; const int opcodeShowVarsExplicit = 0x200021e; + const int opcodeToggleGodMode = 0x200021f; } namespace Sky From c6d2d1999ac79c7c2ddfcb3527b537f9dd75e71f Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 25 Aug 2013 17:40:08 +0200 Subject: [PATCH 104/194] Fix an item duplication glitch --- apps/openmw/mwgui/container.cpp | 9 +++++++-- apps/openmw/mwgui/containeritemmodel.cpp | 9 ++++----- apps/openmw/mwgui/inventoryitemmodel.cpp | 9 ++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 85c2cf5fb..bc869e5fe 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -88,8 +88,13 @@ namespace MWGui mDragAndDropWidget->setVisible(false); - targetModel->copyItem(mItem, mDraggedCount); - mSourceModel->removeItem(mItem, mDraggedCount); + // If item is dropped where it was taken from, we don't need to do anything - + // otherwise, do the transfer + if (targetModel != mSourceModel) + { + targetModel->copyItem(mItem, mDraggedCount); + mSourceModel->removeItem(mItem, mDraggedCount); + } mSourceModel->update(); diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index e23b4f77f..eff8fbcc1 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -74,13 +74,12 @@ ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) void ContainerItemModel::copyItem (const ItemStack& item, size_t count) { const MWWorld::Ptr& source = mItemSources[mItemSources.size()-1]; + if (item.mBase.getContainerStore() == &source.getClass().getContainerStore(source)) + throw std::runtime_error("Item to copy needs to be from a different container!"); int origCount = item.mBase.getRefData().getCount(); item.mBase.getRefData().setCount(count); - MWWorld::ContainerStoreIterator it = MWWorld::Class::get(source).getContainerStore(source).add(item.mBase, source); - if (*it != item.mBase) - item.mBase.getRefData().setCount(origCount); - else - item.mBase.getRefData().setCount(origCount + count); // item copied onto itself + source.getClass().getContainerStore(source).add(item.mBase, source); + item.mBase.getRefData().setCount(origCount); } void ContainerItemModel::removeItem (const ItemStack& item, size_t count) diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index ec3bb3b30..62a5a75f0 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -40,13 +40,12 @@ ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) void InventoryItemModel::copyItem (const ItemStack& item, size_t count) { + if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) + throw std::runtime_error("Item to copy needs to be from a different container!"); int origCount = item.mBase.getRefData().getCount(); item.mBase.getRefData().setCount(count); - MWWorld::ContainerStoreIterator it = MWWorld::Class::get(mActor).getContainerStore(mActor).add(item.mBase, mActor); - if (*it != item.mBase) - item.mBase.getRefData().setCount(origCount); - else - item.mBase.getRefData().setCount(origCount + count); // item copied onto itself + mActor.getClass().getContainerStore(mActor).add(item.mBase, mActor); + item.mBase.getRefData().setCount(origCount); } From e33239eae5677b1e3760422e487dfc60c1b4ea26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20S=C3=B6derberg?= Date: Sun, 25 Aug 2013 23:15:20 +0200 Subject: [PATCH 105/194] Various fixes --- files/mygui/core_layouteditor.xml | 36 ++--- files/mygui/openmw_alchemy_window.layout | 2 +- files/mygui/openmw_button.skin.xml | 2 +- files/mygui/openmw_confirmation_dialog.layout | 12 +- files/mygui/openmw_console.layout | 4 +- files/mygui/openmw_console.skin.xml | 33 +++-- files/mygui/openmw_dialogue_window.layout | 6 +- files/mygui/openmw_dialogue_window_skin.xml | 10 +- files/mygui/openmw_edit.skin.xml | 12 +- files/mygui/openmw_hud_energybar.skin.xml | 16 +-- .../openmw_interactive_messagebox.layout | 12 +- files/mygui/openmw_journal_skin.xml | 6 +- files/mygui/openmw_list.skin.xml | 42 +++--- files/mygui/openmw_messagebox.layout | 14 +- files/mygui/openmw_pointer.xml | 70 +++++----- files/mygui/openmw_progress.skin.xml | 26 ++-- files/mygui/openmw_stats_window.layout | 2 +- files/mygui/openmw_text.skin.xml | 132 +++++++++--------- files/mygui/openmw_tooltips.layout | 2 +- files/mygui/openmw_windows.skin.xml | 86 ++++++------ 20 files changed, 263 insertions(+), 262 deletions(-) diff --git a/files/mygui/core_layouteditor.xml b/files/mygui/core_layouteditor.xml index db917b6ef..007b5e638 100644 --- a/files/mygui/core_layouteditor.xml +++ b/files/mygui/core_layouteditor.xml @@ -1,25 +1,25 @@ - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_alchemy_window.layout b/files/mygui/openmw_alchemy_window.layout index 2241813cf..4b15ac1bd 100644 --- a/files/mygui/openmw_alchemy_window.layout +++ b/files/mygui/openmw_alchemy_window.layout @@ -2,7 +2,7 @@ - + diff --git a/files/mygui/openmw_button.skin.xml b/files/mygui/openmw_button.skin.xml index 4f78f7125..e152a9112 100644 --- a/files/mygui/openmw_button.skin.xml +++ b/files/mygui/openmw_button.skin.xml @@ -62,7 +62,7 @@ - + diff --git a/files/mygui/openmw_confirmation_dialog.layout b/files/mygui/openmw_confirmation_dialog.layout index 024b8ab3b..47e1fd2b8 100644 --- a/files/mygui/openmw_confirmation_dialog.layout +++ b/files/mygui/openmw_confirmation_dialog.layout @@ -1,19 +1,19 @@ - + - - - + + + - + - + diff --git a/files/mygui/openmw_console.layout b/files/mygui/openmw_console.layout index a65e8b4f4..bfda40c68 100644 --- a/files/mygui/openmw_console.layout +++ b/files/mygui/openmw_console.layout @@ -2,7 +2,7 @@ - + @@ -12,7 +12,7 @@ - + diff --git a/files/mygui/openmw_console.skin.xml b/files/mygui/openmw_console.skin.xml index 3537bd662..219cce39a 100644 --- a/files/mygui/openmw_console.skin.xml +++ b/files/mygui/openmw_console.skin.xml @@ -1,30 +1,33 @@ - - - - - - - - - + - + + + + + - + + + - - - - + + + + + + + + + diff --git a/files/mygui/openmw_dialogue_window.layout b/files/mygui/openmw_dialogue_window.layout index 9d54f03d0..78daa0705 100644 --- a/files/mygui/openmw_dialogue_window.layout +++ b/files/mygui/openmw_dialogue_window.layout @@ -15,11 +15,11 @@ - + - - + + diff --git a/files/mygui/openmw_dialogue_window_skin.xml b/files/mygui/openmw_dialogue_window_skin.xml index d4b76daa7..4f68a90fa 100644 --- a/files/mygui/openmw_dialogue_window_skin.xml +++ b/files/mygui/openmw_dialogue_window_skin.xml @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/files/mygui/openmw_edit.skin.xml b/files/mygui/openmw_edit.skin.xml index 76988a5be..b10854b19 100644 --- a/files/mygui/openmw_edit.skin.xml +++ b/files/mygui/openmw_edit.skin.xml @@ -21,12 +21,10 @@ + + - - - - - + @@ -39,9 +37,9 @@ - + - + diff --git a/files/mygui/openmw_hud_energybar.skin.xml b/files/mygui/openmw_hud_energybar.skin.xml index f64cb6ca0..f10908d7b 100644 --- a/files/mygui/openmw_hud_energybar.skin.xml +++ b/files/mygui/openmw_hud_energybar.skin.xml @@ -43,32 +43,32 @@ - - + + - - + + - - + + - - + + diff --git a/files/mygui/openmw_interactive_messagebox.layout b/files/mygui/openmw_interactive_messagebox.layout index 8d6b6dc34..d3ac1f8b5 100644 --- a/files/mygui/openmw_interactive_messagebox.layout +++ b/files/mygui/openmw_interactive_messagebox.layout @@ -3,16 +3,16 @@ - - - + + + - - + + - + diff --git a/files/mygui/openmw_journal_skin.xml b/files/mygui/openmw_journal_skin.xml index 9fc5e4c7f..ca6d309d7 100644 --- a/files/mygui/openmw_journal_skin.xml +++ b/files/mygui/openmw_journal_skin.xml @@ -2,9 +2,9 @@ - - - + + + diff --git a/files/mygui/openmw_list.skin.xml b/files/mygui/openmw_list.skin.xml index 54143f270..02c11c354 100644 --- a/files/mygui/openmw_list.skin.xml +++ b/files/mygui/openmw_list.skin.xml @@ -5,8 +5,8 @@ - - + + @@ -43,8 +43,8 @@ - - + + @@ -89,17 +89,17 @@ - - + + - + - - + + @@ -114,9 +114,9 @@ - - - + + + @@ -144,9 +144,9 @@ - - - + + + @@ -157,12 +157,12 @@ - - - - + + + + - + @@ -171,7 +171,7 @@ - + diff --git a/files/mygui/openmw_messagebox.layout b/files/mygui/openmw_messagebox.layout index 3d91a289a..dfdb57648 100644 --- a/files/mygui/openmw_messagebox.layout +++ b/files/mygui/openmw_messagebox.layout @@ -2,17 +2,17 @@ - - - - + + + + - - + + diff --git a/files/mygui/openmw_pointer.xml b/files/mygui/openmw_pointer.xml index cf21037f8..a55a5453c 100644 --- a/files/mygui/openmw_pointer.xml +++ b/files/mygui/openmw_pointer.xml @@ -1,39 +1,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_progress.skin.xml b/files/mygui/openmw_progress.skin.xml index 635e40fb2..aa994fea7 100644 --- a/files/mygui/openmw_progress.skin.xml +++ b/files/mygui/openmw_progress.skin.xml @@ -17,7 +17,7 @@ - + @@ -25,41 +25,41 @@ - - + + - + - - + + - - + + - - + + - - - + + + diff --git a/files/mygui/openmw_stats_window.layout b/files/mygui/openmw_stats_window.layout index dec962f05..5ae3f96ca 100644 --- a/files/mygui/openmw_stats_window.layout +++ b/files/mygui/openmw_stats_window.layout @@ -219,7 +219,7 @@ - + diff --git a/files/mygui/openmw_text.skin.xml b/files/mygui/openmw_text.skin.xml index 6959c1f5b..6a1dea60b 100644 --- a/files/mygui/openmw_text.skin.xml +++ b/files/mygui/openmw_text.skin.xml @@ -4,46 +4,46 @@ - - - + + + - - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - + + + @@ -51,36 +51,36 @@ - - - - + + + + - - - - - + + + + + - - - - - + + + + + - + - - - + + + @@ -93,9 +93,9 @@ - - - + + + @@ -108,9 +108,9 @@ - - - + + + @@ -123,9 +123,9 @@ - - - + + + @@ -138,46 +138,46 @@ - + - - + + - - + + - - + + - - + + - - - + + + - - - + + + - - - + + + diff --git a/files/mygui/openmw_tooltips.layout b/files/mygui/openmw_tooltips.layout index bec6dd87c..624c133f2 100644 --- a/files/mygui/openmw_tooltips.layout +++ b/files/mygui/openmw_tooltips.layout @@ -200,7 +200,7 @@ - + diff --git a/files/mygui/openmw_windows.skin.xml b/files/mygui/openmw_windows.skin.xml index e8861578f..22586716c 100644 --- a/files/mygui/openmw_windows.skin.xml +++ b/files/mygui/openmw_windows.skin.xml @@ -97,26 +97,26 @@ - + - + - + - + - + - + - + - + @@ -167,7 +167,7 @@ - + @@ -185,7 +185,7 @@ - + @@ -196,7 +196,7 @@ - + @@ -207,7 +207,7 @@ - + @@ -218,7 +218,7 @@ - + @@ -230,25 +230,25 @@ - + - + - + - + @@ -256,56 +256,56 @@ - + - + - + - + - + - + - + - + @@ -390,9 +390,9 @@ - - - + + + @@ -415,10 +415,10 @@ ------------------------------------------------------ --> - - - - + + + + @@ -555,10 +555,10 @@ - - - - + + + + @@ -692,10 +692,10 @@ - - - - + + + + @@ -835,9 +835,9 @@ - - - + + + From 45083fc89e6c0e8d6019cf33fe5611980d769d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20S=C3=B6derberg?= Date: Sun, 25 Aug 2013 23:15:29 +0200 Subject: [PATCH 106/194] Chargen updates --- files/mygui/openmw_chargen_birth.layout | 13 ++- files/mygui/openmw_chargen_class.layout | 92 +++++++++---------- .../openmw_chargen_class_description.layout | 16 ++-- .../mygui/openmw_chargen_create_class.layout | 36 ++++---- ...penmw_chargen_generate_class_result.layout | 12 +-- files/mygui/openmw_chargen_race.layout | 14 +-- files/mygui/openmw_chargen_review.layout | 14 +-- .../openmw_chargen_select_attribute.layout | 16 ++-- .../mygui/openmw_chargen_select_skill.layout | 54 +++++------ 9 files changed, 134 insertions(+), 133 deletions(-) diff --git a/files/mygui/openmw_chargen_birth.layout b/files/mygui/openmw_chargen_birth.layout index 75f741271..e8d959ba2 100644 --- a/files/mygui/openmw_chargen_birth.layout +++ b/files/mygui/openmw_chargen_birth.layout @@ -1,20 +1,19 @@ - + - + - - + + - - + - + diff --git a/files/mygui/openmw_chargen_class.layout b/files/mygui/openmw_chargen_class.layout index 6d159fe61..aac97870c 100644 --- a/files/mygui/openmw_chargen_class.layout +++ b/files/mygui/openmw_chargen_class.layout @@ -1,66 +1,66 @@ - + - + - - + + - + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + diff --git a/files/mygui/openmw_chargen_class_description.layout b/files/mygui/openmw_chargen_class_description.layout index 7050f1e3e..e003f22c1 100644 --- a/files/mygui/openmw_chargen_class_description.layout +++ b/files/mygui/openmw_chargen_class_description.layout @@ -1,22 +1,22 @@ - + + - - - - - - + + + + - + + diff --git a/files/mygui/openmw_chargen_create_class.layout b/files/mygui/openmw_chargen_create_class.layout index 9677f438f..e9c4146a0 100644 --- a/files/mygui/openmw_chargen_create_class.layout +++ b/files/mygui/openmw_chargen_create_class.layout @@ -1,14 +1,15 @@ - + + - - + + - + - + @@ -30,30 +31,30 @@ - - + + - - - - - + + + + + - - - - - + + + + + @@ -72,5 +73,6 @@ + diff --git a/files/mygui/openmw_chargen_generate_class_result.layout b/files/mygui/openmw_chargen_generate_class_result.layout index 9ee3e0701..f7178042f 100644 --- a/files/mygui/openmw_chargen_generate_class_result.layout +++ b/files/mygui/openmw_chargen_generate_class_result.layout @@ -1,26 +1,26 @@ - + - - + + - + - + - + diff --git a/files/mygui/openmw_chargen_race.layout b/files/mygui/openmw_chargen_race.layout index 0f63ba700..1290795ed 100644 --- a/files/mygui/openmw_chargen_race.layout +++ b/files/mygui/openmw_chargen_race.layout @@ -9,7 +9,7 @@ - + @@ -22,7 +22,7 @@ - + @@ -34,7 +34,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -65,17 +65,17 @@ - + - + - + diff --git a/files/mygui/openmw_chargen_review.layout b/files/mygui/openmw_chargen_review.layout index 84b105bd0..408d4f471 100644 --- a/files/mygui/openmw_chargen_review.layout +++ b/files/mygui/openmw_chargen_review.layout @@ -1,10 +1,10 @@ - + - + @@ -24,7 +24,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -106,12 +106,12 @@ - - + + - + diff --git a/files/mygui/openmw_chargen_select_attribute.layout b/files/mygui/openmw_chargen_select_attribute.layout index b847bcd3b..f0f72bb0f 100644 --- a/files/mygui/openmw_chargen_select_attribute.layout +++ b/files/mygui/openmw_chargen_select_attribute.layout @@ -10,14 +10,14 @@ - - - - - - - - + + + + + + + + diff --git a/files/mygui/openmw_chargen_select_skill.layout b/files/mygui/openmw_chargen_select_skill.layout index 90ae477e5..dc1798995 100644 --- a/files/mygui/openmw_chargen_select_skill.layout +++ b/files/mygui/openmw_chargen_select_skill.layout @@ -14,45 +14,45 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + From 5af89a9e8f704b31a9fe1d1ff36f987c18fd0c67 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 26 Aug 2013 00:13:42 +0200 Subject: [PATCH 107/194] Fix an AABB assert for loading empty exterior cells --- apps/openmw/mwrender/renderingmanager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index ade871a94..a7e35fa19 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -652,7 +652,8 @@ void RenderingManager::requestMap(MWWorld::Ptr::CellStore* cell) Ogre::Vector2 center(cell->mCell->getGridX() + 0.5, cell->mCell->getGridY() + 0.5); dims.merge(mTerrain->getWorldBoundingBox(center)); - mTerrain->update(dims.getCenter()); + if (dims.isFinite()) + mTerrain->update(dims.getCenter()); mLocalMap->requestMap(cell, dims.getMinimum().z, dims.getMaximum().z); } From 3f8a69b4ad8ea949b753c3598912b917e791b62c Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 26 Aug 2013 00:14:02 +0200 Subject: [PATCH 108/194] Don't crash when trying to delete a reference that doesn't exist anymore --- apps/openmw/mwworld/cellstore.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 8bb3d3c8d..0c145ab60 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -43,7 +43,8 @@ namespace MWWorld // Skip this when reference was deleted. // TODO: Support respawning references, in this case, we need to track it somehow. if (ref.mDeleted) { - mList.erase(iter); + if (iter != mList.end()) + mList.erase(iter); return; } From bd6dd071aa5c84aab6611a8e9a644c5d2a2050d9 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sat, 24 Aug 2013 17:42:40 -0700 Subject: [PATCH 109/194] Use the non-accumulation root's parent as the accumulation root This relies on the non-accumulation root not being the skeleton root. I haven't found an instance where this isn't the case. --- apps/openmw/mwrender/animation.cpp | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index f44f53a74..545060fe3 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -64,7 +64,7 @@ void Animation::destroyObjectList(Ogre::SceneManager *sceneMgr, NifOgre::ObjectL Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) : mPtr(ptr) , mCamera(NULL) - , mInsert(NULL) + , mInsert(node) , mSkelBase(NULL) , mAccumRoot(NULL) , mNonAccumRoot(NULL) @@ -74,20 +74,14 @@ Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) { for(size_t i = 0;i < sNumGroups;i++) mAnimationValuePtr[i].bind(OGRE_NEW AnimationValue(this)); - mInsert = node->createChildSceneNode(); } Animation::~Animation() { - if(mInsert) - { - mAnimSources.clear(); - - Ogre::SceneManager *sceneMgr = mInsert->getCreator(); - destroyObjectList(sceneMgr, mObjectRoot); + mAnimSources.clear(); - sceneMgr->destroySceneNode(mInsert); - } + Ogre::SceneManager *sceneMgr = mInsert->getCreator(); + destroyObjectList(sceneMgr, mObjectRoot); } @@ -268,8 +262,13 @@ void Animation::addAnimSource(const std::string &model) if(!mAccumRoot && grp == 0) { - mAccumRoot = mInsert; mNonAccumRoot = dstval->getNode(); + mAccumRoot = mNonAccumRoot->getParent(); + if(!mAccumRoot) + { + std::cerr<< "Non-Accum root for "<getParentSceneNode()->getScale(); + Ogre::Vector3 extents = getWorldBounds().getSize(); float size = std::max(std::max(extents.x, extents.y), extents.z); bool small = (size < Settings::Manager::getInt("small object size", "Viewing distance")) && From 0463dc06536795bc0d793d26c11481c27a565718 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 25 Aug 2013 11:03:23 -0700 Subject: [PATCH 110/194] Use a smaller static geometry size for interior cells --- apps/openmw/mwrender/objects.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index 337327dc4..fd81baf6e 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -130,7 +130,10 @@ void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh) // - the culling will be more inefficient // If it is set too low: // - there will be too many batches. - sg->setRegionDimensions(Ogre::Vector3(2500,2500,2500)); + if(ptr.getCell()->isExterior()) + sg->setRegionDimensions(Ogre::Vector3(2048,2048,2048)); + else + sg->setRegionDimensions(Ogre::Vector3(1024,1024,1024)); sg->setVisibilityFlags(small ? RV_StaticsSmall : RV_Statics); From 3843357cd2d9f8370bb7b2a309979c8a0db58d64 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 25 Aug 2013 11:04:55 -0700 Subject: [PATCH 111/194] Fix actor stepping --- apps/openmw/mwworld/physicssystem.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 97aa7dffe..36ea8ccc6 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -29,7 +29,7 @@ namespace MWWorld { static const float sMaxSlope = 60.0f; - static const float sStepSize = 30.0f; + static const float sStepSize = 32.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static const int sMaxIterations = 8; @@ -48,16 +48,15 @@ namespace MWWorld OEngine::Physic::ActorTracer tracer, stepper; stepper.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(stepper.mFraction == 0.0f) + if(stepper.mFraction < std::numeric_limits::epsilon()) return false; tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + velocity*remainingTime, engine); - if(tracer.mFraction < std::numeric_limits::epsilon() || - (tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) > sMaxSlope)) + if(tracer.mFraction < std::numeric_limits::epsilon()) return false; stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); - if(getSlope(stepper.mPlaneNormal) <= sMaxSlope) + if(stepper.mFraction < 1.0f && getSlope(stepper.mPlaneNormal) <= sMaxSlope) { // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. position = stepper.mEndPos; From f2889e4bb53a8fe0c94043faede5d5e43d6ea2e9 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Sun, 25 Aug 2013 15:10:22 -0700 Subject: [PATCH 112/194] Remove unused functions and parameters --- apps/openmw/mwworld/physicssystem.cpp | 23 ++--------------------- apps/openmw/mwworld/physicssystem.hpp | 6 +----- apps/openmw/mwworld/worldimp.cpp | 2 +- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 36ea8ccc6..433fe0892 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -256,7 +256,7 @@ namespace MWWorld return mEngine; } - std::pair PhysicsSystem::getFacedHandle (MWWorld::World& world, float queryDistance) + std::pair PhysicsSystem::getFacedHandle(float queryDistance) { Ray ray = mRender.getCamera()->getCameraToViewportRay(0.5, 0.5); @@ -266,8 +266,7 @@ namespace MWWorld btVector3 dir(dir_.x, dir_.y, dir_.z); btVector3 dest = origin + dir * queryDistance; - std::pair result; - /*auto*/ result = mEngine->rayTest(origin, dest); + std::pair result = mEngine->rayTest(origin, dest); result.second *= queryDistance; return std::make_pair (result.second, result.first); @@ -338,24 +337,6 @@ namespace MWWorld } - btVector3 PhysicsSystem::getRayPoint(float extent) - { - //get a ray pointing to the center of the viewport - Ray centerRay = mRender.getCamera()->getCameraToViewportRay( - mRender.getViewport()->getWidth()/2, - mRender.getViewport()->getHeight()/2); - btVector3 result(centerRay.getPoint(extent).x,centerRay.getPoint(extent).y,centerRay.getPoint(extent).z); - return result; - } - - btVector3 PhysicsSystem::getRayPoint(float extent, float mouseX, float mouseY) - { - //get a ray pointing to the center of the viewport - Ray centerRay = mRender.getCamera()->getCameraToViewportRay(mouseX, mouseY); - btVector3 result(centerRay.getPoint(extent).x,centerRay.getPoint(extent).y,centerRay.getPoint(extent).z); - return result; - } - bool PhysicsSystem::castRay(const Vector3& from, const Vector3& to, bool raycastingObjectOnly,bool ignoreHeightMap) { btVector3 _from, _to; diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index f76b4d29c..3dcd088f5 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -56,7 +56,7 @@ namespace MWWorld std::vector getCollisions(const MWWorld::Ptr &ptr); ///< get handles this object collides with Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr); - std::pair getFacedHandle (MWWorld::World& world, float queryDistance); + std::pair getFacedHandle(float queryDistance); std::pair getHitContact(const std::string &name, const Ogre::Vector3 &origin, const Ogre::Quaternion &orientation, @@ -64,10 +64,6 @@ namespace MWWorld std::vector < std::pair > getFacedHandles (float queryDistance); std::vector < std::pair > getFacedHandles (float mouseX, float mouseY, float queryDistance); - btVector3 getRayPoint(float extent); - btVector3 getRayPoint(float extent, float mouseX, float mouseY); - - // cast ray, return true if it hit something. if raycasringObjectOnlt is set to false, it ignores NPCs and objects with no collisions. bool castRay(const Ogre::Vector3& from, const Ogre::Vector3& to, bool raycastingObjectOnly = true,bool ignoreHeightMap = false); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index eee9c7a19..09dc0493e 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -761,7 +761,7 @@ namespace MWWorld std::pair result; if (!mRendering->occlusionQuerySupported()) - result = mPhysics->getFacedHandle (*this, getMaxActivationDistance ()); + result = mPhysics->getFacedHandle (getMaxActivationDistance ()); else result = std::make_pair (mFacedDistance, mFacedHandle); From 60fa69139e3cf86496900cda5ebf149684b2df16 Mon Sep 17 00:00:00 2001 From: mckibbenta Date: Sun, 25 Aug 2013 19:20:14 -0400 Subject: [PATCH 113/194] minor changes --- apps/openmw/mwbase/world.hpp | 1 - apps/openmw/mwscript/miscextensions.cpp | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 5903f352d..6101358de 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -396,7 +396,6 @@ namespace MWBase /// It only applies to the current form the NPC is in. virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) = 0; - // Dummy Implementation virtual bool toggleGodMode() = 0; }; } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index a8d8a5f2b..c53cafa17 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -726,7 +726,8 @@ namespace MWScript bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); - context.report (enabled ? "God Mode -> On" : "God Mode -> Off"); + // context.report (enabled ? "God Mode -> On" : "God Mode -> Off"); + context.report("Unimplemented"); } }; From 44b4c642b73b1b08027ef13d492603c0456fa141 Mon Sep 17 00:00:00 2001 From: Marek Kochanowicz Date: Mon, 26 Aug 2013 10:12:43 +0200 Subject: [PATCH 114/194] Added alternative book icon in scalable folder. No raster just yet. I'm not sure if it will replace old icon. --- .../scalable/referenceable-record/.directory | 2 +- .../scalable/referenceable-record/book2.svgz | Bin 0 -> 110316 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 files/opencs/scalable/referenceable-record/book2.svgz diff --git a/files/opencs/scalable/referenceable-record/.directory b/files/opencs/scalable/referenceable-record/.directory index c633c38a9..98e1b7f92 100644 --- a/files/opencs/scalable/referenceable-record/.directory +++ b/files/opencs/scalable/referenceable-record/.directory @@ -1,7 +1,7 @@ [Dolphin] GroupedSorting=true SortFoldersFirst=false -Timestamp=2013,8,11,16,50,43 +Timestamp=2013,8,25,18,35,16 Version=3 ViewMode=2 VisibleRoles=Compact_text,Compact_size diff --git a/files/opencs/scalable/referenceable-record/book2.svgz b/files/opencs/scalable/referenceable-record/book2.svgz new file mode 100644 index 0000000000000000000000000000000000000000..4535a2fce4d3af8372368319a0e3d37ee14433f5 GIT binary patch literal 110316 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

;o=p=G%w*nBQ-eZuh#OGY7_GEGs?QM{ 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 M08#8RiXcG)0E35&x&QzG literal 0 HcmV?d00001 From 937f5b11a2795d91a033da953405f6f7b5c7a665 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 26 Aug 2013 12:25:19 +0200 Subject: [PATCH 115/194] fixed broken record deletion --- apps/opencs/model/world/collection.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 526c07815..6cf31d0a4 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -236,14 +236,15 @@ namespace CSMWorld if (iter->second>=index+count) { iter->second -= count; + ++iter; } else { mIndex.erase (iter++); } } - - ++iter; + else + ++iter; } } From 20bd0707dcff43888b0d55006a850ebb8c216763 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 26 Aug 2013 12:25:52 +0200 Subject: [PATCH 116/194] avoid use of column number literals --- apps/opencs/model/world/commands.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 43ecaca63..f6f421c6a 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -69,7 +69,9 @@ CSMWorld::RevertCommand::~RevertCommand() void CSMWorld::RevertCommand::redo() { - QModelIndex index = mModel.getModelIndex (mId, 1); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); + + QModelIndex index = mModel.getModelIndex (mId, column); RecordBase::State state = static_cast (mModel.data (index).toInt()); if (state==RecordBase::State_ModifiedOnly) @@ -102,7 +104,9 @@ CSMWorld::DeleteCommand::~DeleteCommand() void CSMWorld::DeleteCommand::redo() { - QModelIndex index = mModel.getModelIndex (mId, 1); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); + + QModelIndex index = mModel.getModelIndex (mId, column); RecordBase::State state = static_cast (mModel.data (index).toInt()); if (state==RecordBase::State_ModifiedOnly) From aa935ff03d86709a629295e232be743ce8c15e3a Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 26 Aug 2013 12:49:13 +0200 Subject: [PATCH 117/194] update tables on filter record changes --- apps/opencs/view/filter/editwidget.cpp | 36 +++++++++++++++++++-- apps/opencs/view/filter/editwidget.hpp | 10 +++++- apps/opencs/view/filter/filterbox.cpp | 2 +- apps/opencs/view/filter/filterbox.hpp | 2 +- apps/opencs/view/filter/recordfilterbox.cpp | 2 +- apps/opencs/view/filter/recordfilterbox.hpp | 2 +- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 98d23efdb..708d45032 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -1,11 +1,27 @@ #include "editwidget.hpp" -CSVFilter::EditWidget::EditWidget (const CSMWorld::Data& data, QWidget *parent) +#include + +#include "../../model/world/data.hpp" + +CSVFilter::EditWidget::EditWidget (CSMWorld::Data& data, QWidget *parent) : QLineEdit (parent), mParser (data) { mPalette = palette(); connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); + + QAbstractItemModel *model = data.getTableModel (CSMWorld::UniversalId::Type_Filters); + + connect (model, SIGNAL (dataChanged (const QModelIndex &, const QModelIndex&)), + this, SLOT (filterDataChanged (const QModelIndex &, const QModelIndex&)), + Qt::QueuedConnection); + connect (model, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), + this, SLOT (filterRowsRemoved (const QModelIndex&, int, int)), + Qt::QueuedConnection); + connect (model, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (filterRowsInserted (const QModelIndex&, int, int)), + Qt::QueuedConnection); } void CSVFilter::EditWidget::textChanged (const QString& text) @@ -23,4 +39,20 @@ void CSVFilter::EditWidget::textChanged (const QString& text) /// \todo improve error reporting; mark only the faulty part } -} \ No newline at end of file +} + +void CSVFilter::EditWidget::filterDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + textChanged (text()); +} + +void CSVFilter::EditWidget::filterRowsRemoved (const QModelIndex& parent, int start, int end) +{ + textChanged (text()); +} + +void CSVFilter::EditWidget::filterRowsInserted (const QModelIndex& parent, int start, int end) +{ + textChanged (text()); +} diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index 72b2659d5..31904e624 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -9,6 +9,8 @@ #include "../../model/filter/parser.hpp" #include "../../model/filter/node.hpp" +class QModelIndex; + namespace CSMWorld { class Data; @@ -25,7 +27,7 @@ namespace CSVFilter public: - EditWidget (const CSMWorld::Data& data, QWidget *parent = 0); + EditWidget (CSMWorld::Data& data, QWidget *parent = 0); signals: @@ -34,6 +36,12 @@ namespace CSVFilter private slots: void textChanged (const QString& text); + + void filterDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void filterRowsRemoved (const QModelIndex& parent, int start, int end); + + void filterRowsInserted (const QModelIndex& parent, int start, int end); }; } diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index af449437b..273170884 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -5,7 +5,7 @@ #include "recordfilterbox.hpp" -CSVFilter::FilterBox::FilterBox (const CSMWorld::Data& data, QWidget *parent) +CSVFilter::FilterBox::FilterBox (CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index 60d2f038f..2524fa0a3 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -18,7 +18,7 @@ namespace CSVFilter public: - FilterBox (const CSMWorld::Data& data, QWidget *parent = 0); + FilterBox (CSMWorld::Data& data, QWidget *parent = 0); signals: diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index 6b1831e30..c405177b0 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -6,7 +6,7 @@ #include "editwidget.hpp" -CSVFilter::RecordFilterBox::RecordFilterBox (const CSMWorld::Data& data, QWidget *parent) +CSVFilter::RecordFilterBox::RecordFilterBox (CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 6b1b0e9bb..057d69518 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -22,7 +22,7 @@ namespace CSVFilter public: - RecordFilterBox (const CSMWorld::Data& data, QWidget *parent = 0); + RecordFilterBox (CSMWorld::Data& data, QWidget *parent = 0); signals: From 2e9948e86a43498dc0f09babb8ed143870cf17c5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 26 Aug 2013 14:40:34 +0200 Subject: [PATCH 118/194] improved one-shot filter handling; allow empty pre-defined filters --- apps/opencs/model/filter/parser.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 2320aa0be..abdc3bab3 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -47,7 +47,7 @@ namespace CSMFilter std::string mString; double mNumber; - Token (Type type); + Token (Type type = Type_None); Token (const std::string& string); @@ -521,9 +521,12 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) mInput = filter; mIndex = 0; - Token token = getNextToken(); + Token token; + + if (allowPredefined) + token = getNextToken(); - if (token==Token (Token::Type_OneShot)) + if (!allowPredefined || token==Token (Token::Type_OneShot)) { boost::shared_ptr node = parseImp (true); From 3e174df856ac3a2f04ba9720ba4965e06a0bd3b9 Mon Sep 17 00:00:00 2001 From: Marek Kochanowicz Date: Tue, 27 Aug 2013 14:49:29 +0200 Subject: [PATCH 119/194] Adding nomadic1 icons. Wooho! --- files/opencs/raster/GMST.png | Bin 0 -> 820 bytes files/opencs/raster/activator.png | Bin 2297 -> 1913 bytes files/opencs/raster/attribute.png | Bin 0 -> 1788 bytes files/opencs/raster/body-part.png | Bin 1788 -> 1248 bytes files/opencs/raster/book.png | Bin 1336 -> 1599 bytes files/opencs/raster/filter.png | Bin 0 -> 1375 bytes files/opencs/raster/ingredient.png | Bin 1444 -> 1384 bytes files/opencs/raster/light.png | Bin 747 -> 1199 bytes files/opencs/raster/random.png | Bin 0 -> 1892 bytes files/opencs/raster/soundgen.png | Bin 2041 -> 2149 bytes files/opencs/raster/static.png | Bin 1518 -> 1297 bytes .../referenceable-record/miscellaneous.svg | 965 ------------------ 12 files changed, 965 deletions(-) create mode 100644 files/opencs/raster/GMST.png create mode 100644 files/opencs/raster/attribute.png create mode 100644 files/opencs/raster/filter.png create mode 100644 files/opencs/raster/random.png delete mode 100644 files/opencs/scalable/referenceable-record/miscellaneous.svg diff --git a/files/opencs/raster/GMST.png b/files/opencs/raster/GMST.png new file mode 100644 index 0000000000000000000000000000000000000000..f246202882eae9203e9a718ee5117031b8e76aa0 GIT binary patch literal 820 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 delta 2285 zcmV004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv00000008+zyMF)x z010qNS#tmY4f_B94f_ELcQu;;00+rQL_t(o!|hdRaFx{=et*9G`|iHq3rT<*66ykh zu#8&cuqvgt(spF1;}{D{ROqOs4(QbJ2V)&OPU|0L>;`pMgpiaVkOn~ziy_d0M8L2o zKrq|A3HN^6-Ol;G{=m!7$|w`;Kc8P`&b;q=p7WmPeGl**zQg|sYu^29^{vFQ-xlFZ zD_@#WJg;T<&VTLW?x;Li6fuAPe9!Xb&sW|okH1~wWy_Yya~_(#tzulo+)33{PaZhX zx`Sbu>53%Fp-`Zv_Nm(Q^XC5G(^xDrcJ$ct880k*@#7ownQ!Cvo9Z|2nOHq>j-eaS z4PAA)oF3b@K}Z3brYD?EN7%A~bEnVT`_!Um4&Ru|-G2`8_Lld*9}0x8UbxuFPo7-; zBZdVmN+nfQkxr*TEec7IoeaZ5*L16?vFX)$^B!{7F0QTGxxKmjwkoi(e#6!Y6DIsv zO{Y(bqHs4a3QTwRWvF@@xm*^65ZJa2p5x&0_z{gnF*rCxB}uecmczjV2WP#$>Wxo^ z?SWiAJAY(R%K82N@dc8{g{}+GbQOlKBa_L36bi5zK;U^~^Eu>lIh2)^aV*1fnyOJ> zFyOtdCwRYU`}XRoRh1>BrB!|Xy-206LQ^%UstPr&B9%(ng?yeNgu=FMSQbIR7X&5L zYHK@G^P82geso&`c76Ddnt(sJN|HpWKhY0O*MFdC8WQ~p>tAh0U)!`{%j{?@GTq@+ z;tb0`5O{DL50}f$h_ZZs)~uOEOLI%#usrbLN4s}~!{Hy=u-RNLi>^x-Ff=%X!NDP% z>NtJyy><2X7h@Ma^F$#MjqpC72cb|HW8)P7!yuQ<<*ha%!l9~AC_GueWjemrFrkR0Z7?#uJU`tEOAPk5Y;#f#>4ulY}EC)#v zF{&&Mp679*{p4r

{MD4u9)E4{ZXz{EYtir*{^N7&)RulH?~X%YtF*=pX2VVd%)1 zCbIcFC?x;@!!$57B%`#nM52_!@At9CkDuth6z zC=QP0c5G|f-qqIHW)v%by?+fAhulRe%^QY+Y&HwiG@8Qa|4+z9}` zTJik;Xf!h2>2$#Dabr~3XfR-H!!VP*Jw0oeEPnQ709d#2Z>yXx*Q_^Iy*cCP(WB(+ z<_sT;MSLS;BO{_D34|0tCbwfigmjk618wmL) z-8tdTlhuIm~A%rY(I-Eh4Wx;VAL`gt(^~6V1O`pectiTH#B!5waAP7(-2Uw0pPfyQ| z4I4Hbx)rjUq4*gwM~?lw)93S!A(UWXpdX3B0qD90)5u(_hDE_~ECf-2Ad0AnSAyqx z5K3{j<80ltbxZ#;905E4ELgBWRurWyl}f3)q4x)afl5LN2qj<`27<(Y+1G%;i6|*4 z1H&*#CX(G=pMS5^<#9!ZTLBILIyySYnE2?!3x2ln=}A>pe-A~%KlixZ9$9uk5JX6# z2$$0hzb}YrGzOMs(f;YNm5XbaJnC_JH^02%m8RhexIPC~{CedF#kC|!A%KCVY5fkx zK_ZbDLZL7$N{0;7)OcP%AQ-9ufYqy4pBXOse+ek!@PCmbg?zq1x-WHanK$R*m|>c; zDWSHS);@lC?)>1MJ$vrSWK3OgD7WPDVPWZuOC7~^eZzY-yZ5z_OppnF*7YNGc7YQR536*Fg7|hF)J`K zIxsM}cUt8D000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs00000NkvXX Hu0mjf&!0hG diff --git a/files/opencs/raster/attribute.png b/files/opencs/raster/attribute.png new file mode 100644 index 0000000000000000000000000000000000000000..4aa5dc02e56575de248710bcf7976dc807f9b796 GIT binary patch literal 1788 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 RU;qFL002ovPDHLkV1f;u@GAfS delta 1317 zcmV+=1={+*47duAB!2{FK}|sb0I`n?{9y$E0004VQb$4nuFf3k0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv00000008+zyMF)x z010qNS#tmY4f_B94f_ELcQu;;00an0L_t(o!|hgWOj}hLet&N7rKLcjv_M+j_^44n zG6xKXGjUK*V$fI%V=arr#F)&nOfvtNX(E}Lz>>u*j;Oe}O|y`U#F#82^UwY*Qxk)Y zVpJFmjmQ97TxqGLrRVj>y=`wl2QFz$-23OA^PH3Syzlco=iUq4kN-mS8d1n6C()a1KDad+<&cN{PQbV zS#aUtt1sfh>2rVru)bi0a^9$GrZs6um)7ahq7?zfn;JE%B3JVIVPP{Yl z*1z_^@i$%{I`-PJ+4V|mGG{45ex+U zpM3mj-GAuC(Wxyl&}Hi?u-R-cGa?WOL|8pKF@Pw-2+44Y!$E|-t$RBx7R$~}7jU%y=+jTO zw;doz4hJHghM$bYYZsk|*AsC~exAzO{oF{hvw!$_z-%_F_uKY;QC3=39PTXIQEbO@ z95RMj!ajmduiLII)Xsi=4j-!Tc($WoPFt!ZEHY;?v@=TX3AuNQiu8JOyZLN_1UqVK z-pk3&QOaqHUO~u`rN%|3l6!*5x}kV~I%6ZSQ`n@5n1}3RzhS@yf9z^u{KM z^++s3iQbVLR>TAa*XZ-EU*H=W8iXT#hktLFOeRfKd&rCwy;?|uQyd=uvWhSeCx^CB zTR3-XZc!*HG469ZS2PpfPC&8|q)$2ksTgS%=|nPHWX!@>4dQ;1#!@0NhnO8im|yr~ z)#Gu076=Bl!m20GQe9OIgTa7IMV7b^rhXC3HntbYx+4WjbwdWNBu305UK! zGc7PREiyJ#F*iCgH##yfD=;!TFfcirw8{Vg02y>eSaefwW^{L9a%BK_cOG{xX>MzC bV_|S*E^l&Yo9;Xs00000NkvXXu0mjf%~V>i diff --git a/files/opencs/raster/filter.png b/files/opencs/raster/filter.png new file mode 100644 index 0000000000000000000000000000000000000000..94a57ecd978df262a337ad721301d4aa8e385ab3 GIT binary patch literal 1375 zcmV-l1)%zgP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000EeNklrYx|9LM`7^iOdAKwos; z6~zlGRpt#XC{_iF2#DZB?Toe(u?T`4sMC0fwa#0sv1w;DMpIH}yeyTpOUD|wkflpa zd}E@Cw#o9{^PCN$6l)Q}i<58;&pDs(^SgY{?_2_K$@};!EbKBrUm)P|a=S}P#Lx5d z3fE;al~W~xx}A1vk`s8rvgL{tW;RDmMl4HOrblJjzq zbs)kZ6qZ6n&Lt8VDy2#gVUWw!P%1R2QPn|uQx1hfjoY{HqNb+qLuyL;&NVJ?oU+`4 z7y(4NwXvc3iWII!+#JJ84WRkR#w(PsniAxx7!_9Tl*E8o4;dw`%k=n{TGA+5vHeq0-rD3^9Ufj zc5H4sLGp`?j4bkJ5vr@T0qy)Ge0B8|1_pj4>Clk0DACqtMr=&H!y|weU0!a%{+l;{ zGb#rL#u+h1|L2&fg{Zik$$5pO3N0;np=&ilueUHM>Fn$!q3uV#wwdH!OKL}pnZ2NW?sTB1XN06~2nj6qK4qRNLb9+zr!yWo94@?k`3JVP-eP0pHztHHUc7?c zzKxZYU-0*J^dcuUoh6kjk57g@7;qjh@0fHM%iEAX zaY4EuBRzAnzyAS~^I*W$>PA}ilfK;0rvSPzD>HkC<~Wd?)3CR-b%DiV@0@GJ-V2D2 zUn$@V4=!486mBy0g4pi?J2Cl+-va0jysR8Ud%MN&YInnB#8DFxk_>0TPx-)Ei;|N$ zj={mnFK#tAys1f#=%`r6?E(@&+a`^}-T#t4cf$<^8|`e0nf@Mr-{1z6h$IIc9X96Mfs@MJ_ZRG5e_;Xa#FTetQU@wk z1LETo--TQN88DqjGL9=0b!5&@a)w+$WMuTZ(bxl-OoQ;RBi2JMAU2j|&Cce(O;5{o hMn%OiADS0A{sp&q2Fw_9TLJ(8002ovPDHLkV1fy8frtPA literal 0 HcmV?d00001 diff --git a/files/opencs/raster/ingredient.png b/files/opencs/raster/ingredient.png index 6b36d008d26b94b7c0c1f1c2d6f215f24ca08c94..564a9304793f03613df2e973144ede4bb60a9615 100644 GIT binary patch delta 1361 zcmV-X1+My}3+M`v85ITq0047(dh`GQ00DDSM?wIu&K&8HArpTIa7bBm000XT000XT z0n*)m`~Uy|8gxZibW?9;ba!ELWdKlNX>N2bPDNB8b~7$DE-^4L^m3s900h@bL_t(o zN41yhPm^~P$NdxbC$NOeQ`$Zh=>0);(XCpgnrKXp6~gdUk?Dl zf%p0OFe)laXXiDkt=t)Vo?yw6O|GUNPFxp=Vm1^8&g6T6%e3(-g+6|q0{bePw9q-8 z2{)eSQ=eSuoy~!Yiz!g%Re?-Pto?5VP@R5|nk{J6Irx9Rx>hp`{Zs(oFO|T+TrPYy znE_wivw(L{585;?lzb`yV+L=_;N)80xd2l}0;x1|$K7R;jq6jnF!u5|+*&Av?#C|p z^1cPG-ZR6MQ4`d3q|orjehri~N$^8zo6Y3{k04p`Kq9Q&I4@|Y0xrm z3k(1JwiZ&|{MTwLhul{IY74L3RV02hyif{Lf1H8A=Xucn&>j|U&f?RZBfPdt2}&bp z#DD8pDWK4EReA@xeC>e;rq^m=WU&~!rkr8n+lGI`nIrt-O$}HM@n55Y5fy-4S2XIj ze)Lt-a`#MULB}IUSor0;MrzI++Hl+Ovz-c17>R)H!sAcWuUeZ8@rQl&k<0U?Zw2(DaLrkghM!HJ9W|Ed~bhB zfZ=fFC_BQdJH(Xyv3u@*P!zk0MJp(OxWnQPN7<{IcsAT$@s&MUzUG?$;D%YmtRc+sg8N}0VB}Zcr)MqTomw(@`q!{ zq~(%~@S1BPkZ59FF*!fTgw&gTq;J*(En_zN0l8neqlo(t<@_o((g!G@PUX5k`_={> z4;=w;e7&;mJgb5S5^YS#%x64CITfNeJx{ZC_8bit30xqKeiBLN!N(|_x7dI4B$G}! z+;gT?L$_D93BlkZHYwSg_5%-i5een7OEoPz(Al|+A%lC4tCl2-eknIFhfUa9fR2@_ z37@y$2!(Z0FuHiF){por2ahB`dJzdFULk!_?S(whJ2`);EOCp}nvKYf?*`r(fm}zd zUAkog=i$V+QZ3QoUo93#eKaxn3wOT~GyUHI>Ni>gnH>E}tcqimTufgW>EHhaNE@=hQK5h>h)_@~Ai9Vqx~VY^w>e?rq-@Kkw6)zX*~+9HIotN zyUu?d1VO%33XuhJ(8^xp1tPIaJ`Iw0E}o8uAX9_S`;1kf_7Je*Vxj)~A1|yRGbt=O z1eJt9khB7V9N;s6t9F9FQ4f zi`!vE>WHd-g2KuYY0zRFre-j%q8AS&73nec!&KG8O1nUXsv00RjwFC7Km#~AJ3tbs z5I|?$5LI#xD>h((NWK!qYe!TXV^4QQNu@C&ZA8r=rDcpE(qW1_A{(#LO1t&}hK8#N zauc~gfhf|V0Y=&r3L@YEv<2;mLQMo9|N4BCK#eMB50`fDmlz@iN)%uy>HHUfsWDT; z`bdE~0+3Yn#MYWA((Y)`C{Tt2KZPn(u?{2Y+*e}2oigBnQ4$ahI51kMLGcubLN%&L ziz(Eg`3iKQ8eOD}5VVH3UuP<VoNvF8sG3Ns9il34 zCdthdg*6fQEH}pi{cYEniW`Z*pJofA^%}Ef>|hHZ8CtQc+C*-)(3?i-RRg3Z3%#aI}1^SM8j&IL&(WHFLJ-kvVIFmC!{Z%Mt~?jm|?9WLc( zIdASY*@Hq$BW0$-^|}Vxl#9i zfwms8Chx?3HW1=*JRyzp&(?_$jv)_iwNr!1i&kY2ucQ}JhcM_;~W4gw6 z(OTOGinbJ{*1OGSTVk`wij>6Ua;oT{{$XE#S;)YpM;jC!xW2I?M^-1Nr{v>~o}Qku zoh+tcZg-G6lF2&5d`R+`o)cm~FN3G2gs$VqojRSS&V#yRfd{9b9f zds}G?XGvQ6+&t!bocaFb)#e9`IUf{K6=6k=u^34sV0*#dO`#hat4bLJjP_hwB|rNvQ6{@|41(x|Mg8-lW2FJ1i0zIVXQ?G%zyg;^QGY+NQk8zcw; z4@bh_Xc!`b9ge{vz(Rz=5jZ&fhqSN2bPDNB8b~7$DE-^4L^m3s900bUML_t(o zN4=L{OjB1J#~b3Si@qTEL0q@z$uZTVCFSP)TRQ^y7b7&0LI8Q|zp z7?4|{Y+?{m1R;Mi0Yy-dKSRNV4laX%OASUt7T>-*w~2k&mXI~ec{mR@x#xU7-}5`? zo^yEsc-FSqSo=Y)S=*oMXEi)pZZEB=+55J-eD7O%guT>S39ALPN&Adj8#XX4*dQTh zpOLCSy3W?EwsH@)XLxu$!b4;Jdq|eqx>+M()eNX&?eu@YKHi0mdLA~<^Dy7G3yP2J z^wb1oVf&f&ejBW3^YBv6!*Zz&!X^6|Y6590TeqVjcCRk_?Z)?RyAkJQx0NWRSb}#+ z`3?>`u33}(T_3B~fL5bKdviTn^wntowh}E3RnRD-$(aBBn_`s!2j4a=;nnZYU}ER6 zX`DuVMKOOp0Yg(AOe?cknHk2?mZpcz$vOCgzN`53L>fH- zeRU}o9}ijJ!#A%WJ0pdjz=g`MF#mACoScK-?J|Ghluk=epsu_KGo!aH@cyf<$VyA3 zCs6Zs0VYZPx#S%D)|Dn4Pl=}|P+5|P@qsQ2{JP;gq-)jm1S*PhasPIwIXMUKY`K7A zi7I*mWd-MOucyNT@3{00Qsb5M1kUH3!C+UrIXMS6=qr(|R?rhD&dnk9Z?nK#>&uWB zNAZ6+E36ulY9Cr9sfL!$+*uPB5%Mn+HIOLFvHYi%$m=iqgv zc~HqC{~rNGl(;A|RK##CB1FW52aA}nAR!ZaM972$3Yp*lCq@-5B_AFcQZymZ9t}c* z@(4l$-68dLWc+;ujK8;l@$(Wez7hfB(#ygm4g*C9T6<`@0_#QevVEH%OP#h)9+ z*j_Kh>(zX0E$8B;@htxMB?lV|C$aW46RUGNEYGB3X(|cNAH|b$v7>-S>3n1L=K{R>tppp3YOKwxurjM8uo!XO84T7sBO(TG^BTEr&C3EB8qA*)t6u`0PEtBi7F6*5OQIzqrk oh4WciD4!+CjUMg}|N0007YG|xzM3qDoB#j-07*qoM6N<$g38At`Tzg` literal 747 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyacIC_6YK2V5m}KU}$JzVE6?TYIwoGP-?)y@G60U!Dss^Zy@@{=eG)|JtPgpN{?i`||&{Q~#eY{CBJ7|LwZ} zua^D)aqs`vz5gHd{(n~T_io|;`xE~Ec<}%0zW)z8{y#4Gdq4mGz3%^iU;O`e`v1e; z|Bnj(-pl)czyJULSO0%J`Tuy{zh||7AJzVUwBY}3^IumT9v^aeeWC8n#imCG?S5aj zJ-x>0?+w?#H{Ji<^!Rfp`tQS{zt8*sKgj$0v}xZ${T;K-ch0f=ddcJ5M%%L+tpD7O z{QDs9&y(iA4@>_(p8)hudYNSnkg_TX@(X5QU>38oW6=uPxhYsKaPr4D-TE2({{G#q zmwxo$-y_ogoBsaYz^qpD`*#)d^vi$$UXJ(@7Z*6~)^k4TvPuR!pec+=-tI089jvk* zKn`btM`STDW`2V(BX_y!Y@pyAPZ!4!i_>pUxk@z!NVGoW){sbOU}Rg+#K;!wwfVoP zWx#mQ{&{c%6UuqIPNT3vuaa; z;exzbyO?Ev2v^uYXk_^5D8XiTNaSeYmAHKYp$6@oi!yY9?o=&tjVMV;EJ?LWE=mPb z3`Pcq#<~Wkx<+OphUQjAKxCwCU}R-ra6eKu9z{cLeoAIqC2kGtSe8BpYLEok5S*V@ pQl40p%1~Zju9umYU7Va)kgAtols@~NjTBH3gQu&X%Q~loCIGkaSGE8E diff --git a/files/opencs/raster/random.png b/files/opencs/raster/random.png new file mode 100644 index 0000000000000000000000000000000000000000..2667630f5c27ba416a3810aab36cbf9d8e872b3c GIT binary patch literal 1892 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 JPDHLkV1mt#-P8a8 diff --git a/files/opencs/raster/static.png b/files/opencs/raster/static.png index b53be12d9a8abad9272fdd7c6a75b1d3a6a494ba..aedf2d30eeed220acb1c5eb82b457f29354ed124 100644 GIT binary patch delta 1262 zcmV1;MAa*k@H7+qQF!XYv000DlNkloADTs|mAvH#$F&d3VqcIwd(fBWXFzH`^ z;9=qk&)c<-lblRuK8S(L!^(tj_-5_B*FJ0SqeMit6Ky4D;%Xt2d9jhp>2+W!mD;_V z&F(zu>e7#aH-WWGMy~?P>GU4nNTu{pELMMn&xb@ZsUIW~`aWkF}% zo+mmx^<7{t8r{R}KFX2EUy9^^o;@pne*n+ZY1!`Wm0wSvmS2FMPo0vVfFE-?`5~K? z?}6{Sy5tr7{Y@q#Ujtv^oiBkeQYrZy_$--}Pk{=aeVjG5nSj`MZ(Ge*rwrW;Mxwe*?CV z!TSiHRpumT<_w@!<|Jq4WB9FBnUnk-V3Gk;lF9lVRA3GP5VcE}WESP0LFuP~3P`6e zT-Yl+iHW&$=j3HPzrBwM1a%AFv+T^Axnw2DK_D5_Mbig^L$u3VIsNN1eYCBm+sce+h_2P z^Drp-yOXyU5zs-JGYDpXzT)@GGSKJs?kY|-r@^v{s8?_@4jL?tCeuuFR>zRP?(@m8 z->(%%s?%UuK?#>(#Rm02c`}%hwR$D$0EnXPgab z>us`r==I72;GV}LgI=#z++xfkah!UyzW(8~(&rea?h4zoxcrr|1F%@KgDIm>Ryr%s%Z9ut6l zTlqgjrCGy`VgDNip{==1K8XMd9UWSYgu^d*SDT4MG;bDGfp%P72+nGjrK~1%lmvg*WB)19yNA58}2&5vNLm%pLe3o>!~g&Q delta 1500 zcmV<21ta>A3hoP#B!2{FK}|sb0I`n?{9y$E0004VQb$4nuFf3k0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv00000008+zyMF)x z010qNS#tmY4f_B94f_ELcQu;;00h2CL_t(o!|hhjZ_`K={(ojXaoadfaZ%b7wWv@v zqC!Z(1%W8)ffK##UxW6-Wp68S=dyPWfH-nN2&o*P2o(VmAP`hE6$?qzpRwb_es2$n zCyo<`?s<2l(TvB>&+pCq=9^Fav9!d@3_6^L8=p6tMGZ=t$9)BCI2lgNU&6 z@}&+gX`>tmnL>egU%wVYqhwYq#z(70y9He7KCXE ze|wuX1`a=bkehe!c0tnaBFb=NdKwc~u5kIm1Cg)+06_r2jKYl@yz}Z68b5yY)Et0} zaAKc5i%hXdmB){D=~Ym#BdFI?w_3R_!d8nz*A@CS3@8t^S_nN4q35Og3Oo<8(Lm5> zXg?B(T7Mq3c1Hmn2F*K1GFn&JQVF?INq-z|X_Tc|EJE3~u;%BHdY%DfY#Z6RImodw zy-tbX#%5<>7K<32nbB+F=Hem)T_SOomQerl1#Y?AmsK>MSY8GP0aaI5We-usY}EKT z@)s^p<;fEnB^CD*1*Z<)zwg<`?3pt-`1nx=Fn<7e7@3(t7=}3b{J94cacrn#$1r;O zH0m20=|>qcA{s;($$7l?$yyG=Mo-o$7*oo&dyIr>!pP-d(D?aN+o9tt z;)hJ303(+Z&ZSFj-eHFLeR(;Ivs(WAdEr+o?3T+tY)7%VRVoPzLq7QQDQ+XElO6$3 z`8-BvXR-U{O^*WVTy$E6scH-m48X|ev9rD|ysfSDZO&vsLBKmNUL<)$15k~RV}HW3 zXm5R84)CGvWFLueKN1E3?nsyzLRL|eN4e#Gh32&OD>=ph#Gkirg^fxD+#wL5ln9$m ztyqkF9xMd>TCJTq5nvdCnGv>H-CE}J%wdS;-k#9JlgWsn+2lAm38mog?R8bLyRjj+ z9zH|_9>iW|ipcX2cwX24pxdr*jH#x73%EW^ zBo&c8KTo;&`EK?x)dRGNXw-3FpE^Z-;ESDxF~>nTH^-)Bi9x|JfZWNG$bS?H+}zxx zNOg(7?%er}V-%?!N6*ZVrYT#E1|d$%vG)KV45KaQiF$+UFkWm&G!YFuLI1b^PWuNL z>S^MF3P{KR001R)MObuXVRU6WV{&C-bY%cCFflVNFf%PPF;p=yIxsdmH90FVGCD9Y z`o*k10000bbVXQnWMOn=I#q3NWNBu305UK!Gc7PPEi*AxF)%tXHaayqD=;!TFffal z6oLQ%02y>eSaefwW^{L9a%BK_cXuvnZfkR6VQ^(GZ*pgw?mQX*0000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From d3d6dfbde8e0296bf35ebd9c1e501062193bed99 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 27 Aug 2013 15:48:13 +0200 Subject: [PATCH 120/194] Refactored loading screen - Add loading progress for data files, global map, terrain - Refactored and improved cell loading progress --- apps/openmw/engine.cpp | 38 ++-- apps/openmw/mwbase/windowmanager.hpp | 7 +- apps/openmw/mwgui/loadingscreen.cpp | 197 +++++++++--------- apps/openmw/mwgui/loadingscreen.hpp | 35 ++-- apps/openmw/mwgui/mapwindow.cpp | 15 +- apps/openmw/mwgui/mapwindow.hpp | 7 + apps/openmw/mwgui/windowmanagerimp.cpp | 72 ++++--- apps/openmw/mwgui/windowmanagerimp.hpp | 10 +- apps/openmw/mwinput/inputmanagerimp.cpp | 171 +++++++-------- apps/openmw/mwinput/inputmanagerimp.hpp | 7 +- apps/openmw/mwrender/globalmap.cpp | 12 +- apps/openmw/mwrender/globalmap.hpp | 7 +- apps/openmw/mwrender/renderingmanager.cpp | 7 +- apps/openmw/mwworld/esmstore.cpp | 7 +- apps/openmw/mwworld/esmstore.hpp | 7 +- apps/openmw/mwworld/scene.cpp | 123 ++++++----- apps/openmw/mwworld/scene.hpp | 6 +- apps/openmw/mwworld/worldimp.cpp | 9 +- components/CMakeLists.txt | 4 + components/esm/esmreader.hpp | 5 + .../loadinglistener/loadinglistener.hpp | 35 ++++ components/terrain/quadtreenode.cpp | 7 +- components/terrain/quadtreenode.hpp | 4 +- components/terrain/terrain.cpp | 12 +- components/terrain/terrain.hpp | 13 +- extern/sdl4ogre/sdlinputwrapper.cpp | 13 +- extern/sdl4ogre/sdlinputwrapper.hpp | 2 +- files/mygui/openmw_loading_screen.layout | 3 +- files/mygui/openmw_progress.skin.xml | 15 +- libs/openengine/ogre/renderer.cpp | 20 +- libs/openengine/ogre/renderer.hpp | 2 +- 31 files changed, 518 insertions(+), 354 deletions(-) create mode 100644 components/loadinglistener/loadinglistener.hpp diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 62a15fbf9..a2eccbaf9 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -123,6 +123,7 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::Environment::get().getWindowManager()->wmUpdateFps(window->getLastFPS(), tri, batch); MWBase::Environment::get().getWindowManager()->onFrame(frametime); + MWBase::Environment::get().getWindowManager()->update(); } catch (const std::exception& e) { @@ -385,26 +386,39 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) loadBSA(); + + // Create input and UI first to set up a bootstrapping environment for + // showing a loading screen and keeping the window responsive while doing so + + std::string keybinderUser = (mCfgMgr.getUserPath() / "input.xml").string(); + bool keybinderUserExists = boost::filesystem::exists(keybinderUser); + MWInput::InputManager* input = new MWInput::InputManager (*mOgre, *this, keybinderUser, keybinderUserExists); + mEnvironment.setInputManager (input); + + MWGui::WindowManager* window = new MWGui::WindowManager( + mExtensions, mFpsLevel, mOgre, mCfgMgr.getLogPath().string() + std::string("/"), + mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding); + mEnvironment.setWindowManager (window); + if (mNewGame) + mEnvironment.getWindowManager()->setNewGame(true); + // Create the world mEnvironment.setWorld( new MWWorld::World (*mOgre, mFileCollections, mMaster, mPlugins, mResDir, mCfgMgr.getCachePath(), mEncoder, mFallbackMap, mActivationDistanceOverride)); MWBase::Environment::get().getWorld()->setupPlayer(); + input->setPlayer(&mEnvironment.getWorld()->getPlayer()); + + window->initUI(); + window->renderWorldMap(); //Load translation data mTranslationDataStorage.setEncoder(mEncoder); for (size_t i = 0; i < mMaster.size(); i++) mTranslationDataStorage.loadTranslationData(mFileCollections, mMaster[i]); - // Create window manager - this manages all the MW-specific GUI windows Compiler::registerExtensions (mExtensions); - mEnvironment.setWindowManager (new MWGui::WindowManager( - mExtensions, mFpsLevel, mOgre, mCfgMgr.getLogPath().string() + std::string("/"), - mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding)); - if (mNewGame) - mEnvironment.getWindowManager()->setNewGame(true); - // Create sound system mEnvironment.setSoundManager (new MWSound::SoundManager(mUseSound)); @@ -422,16 +436,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mEnvironment.setJournal (new MWDialogue::Journal); mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mVerboseScripts, mTranslationDataStorage)); - // Sets up the input system - - // Get the path for the keybinder xml file - std::string keybinderUser = (mCfgMgr.getUserPath() / "input.xml").string(); - bool keybinderUserExists = boost::filesystem::exists(keybinderUser); - - mEnvironment.setInputManager (new MWInput::InputManager (*mOgre, - MWBase::Environment::get().getWorld()->getPlayer(), - *MWBase::Environment::get().getWindowManager(), *this, keybinderUser, keybinderUserExists)); - mEnvironment.getWorld()->renderPlayer(); if (!mNewGame) diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 77941a43a..1cd867223 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -9,6 +9,8 @@ #include +#include + #include "../mwmechanics/stat.hpp" #include "../mwgui/mode.hpp" @@ -253,9 +255,6 @@ namespace MWBase virtual void executeInConsole (const std::string& path) = 0; - virtual void setLoadingProgress (const std::string& stage, int depth, int current, int total) = 0; - virtual void loadingDone() = 0; - virtual void enableRest() = 0; virtual bool getRestEnabled() = 0; virtual bool getJournalAllowed() = 0; @@ -282,6 +281,8 @@ namespace MWBase virtual const Translation::Storage& getTranslationDataStorage() const = 0; virtual void setKeyFocusWidget (MyGUI::Widget* widget) = 0; + + virtual Loading::Listener* getLoadingScreen() = 0; }; } diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 547d2fe29..9b63dfa76 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -19,20 +19,16 @@ namespace MWGui : mSceneMgr(sceneMgr) , mWindow(rw) , WindowBase("openmw_loading_screen.layout") - , mLoadingOn(false) , mLastRenderTime(0.f) , mLastWallpaperChangeTime(0.f) , mFirstLoad(true) - , mTotalRefsLoading(0) - , mCurrentCellLoading(0) - , mTotalCellsLoading(0) - , mCurrentRefLoading(0) - , mCurrentRefList(0) + , mProgress(0) { getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); getWidget(mBackgroundImage, "BackgroundImage"); + mProgressBar->setScrollViewPage(1); mBackgroundMaterial = Ogre::MaterialManager::getSingleton().create("BackgroundMaterial", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); mBackgroundMaterial->getTechnique(0)->getPass(0)->setLightingEnabled(false); @@ -54,6 +50,11 @@ namespace MWGui mRectangle->setVisible(false); } + void LoadingScreen::setLabel(const std::string &label) + { + mLoadingText->setCaptionWithReplacing(label); + } + LoadingScreen::~LoadingScreen() { delete mRectangle; @@ -64,56 +65,108 @@ namespace MWGui setCoord(0,0,w,h); } - void LoadingScreen::setLoadingProgress (const std::string& stage, int depth, int current, int total) + void LoadingScreen::loadingOn() { - if (!mLoadingOn) - loadingOn(); - - const int numRefLists = 20; + setVisible(true); - if (depth == 0) + if (mFirstLoad) { - mCurrentCellLoading = current; - mTotalCellsLoading = total; - - mCurrentRefLoading = 0; - mCurrentRefList = 0; + changeWallpaper(); } - else if (depth == 1) + else { - mCurrentRefLoading = current; - mTotalRefsLoading = total; + mBackgroundImage->setImageTexture(""); } - assert (mTotalCellsLoading != 0); + MWBase::Environment::get().getWindowManager()->pushGuiMode(mFirstLoad ? GM_LoadingWallpaper : GM_Loading); + } + + void LoadingScreen::loadingOff() + { + setVisible(false); + + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Loading); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_LoadingWallpaper); + } - float refProgress; - if (mTotalRefsLoading <= 1) - refProgress = 1; + void LoadingScreen::changeWallpaper () + { + if (mResources.empty()) + { + Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); + for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) + { + Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "Splash_*.tga"); + mResources.insert(mResources.end(), resourcesInThisGroup->begin(), resourcesInThisGroup->end()); + } + } + + if (!mResources.empty()) + { + std::string const & randomSplash = mResources.at (rand() % mResources.size()); + + Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton ().load (randomSplash, Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME); + + mBackgroundImage->setImageTexture (randomSplash); + } else - refProgress = float(mCurrentRefLoading) / float(mTotalRefsLoading-1); - refProgress += mCurrentRefList; - refProgress /= numRefLists; + std::cerr << "No loading screens found!" << std::endl; + } - assert(refProgress <= 1 && refProgress >= 0); + void LoadingScreen::setProgressRange (size_t range) + { + mProgressBar->setScrollRange(range+1); + mProgressBar->setScrollPosition(0); + mProgressBar->setTrackSize(0); + mProgress = 0; + } - if (depth == 1 && mCurrentRefLoading == mTotalRefsLoading-1) - ++mCurrentRefList; + void LoadingScreen::setProgress (size_t value) + { + assert(value < mProgressBar->getScrollRange()); + if (value - mProgress < mProgressBar->getScrollRange()/100.f) + return; + mProgress = value; + mProgressBar->setScrollPosition(0); + mProgressBar->setTrackSize(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize()); + draw(); + } - float progress = (float(mCurrentCellLoading)+refProgress) / float(mTotalCellsLoading); - assert(progress <= 1 && progress >= 0); + void LoadingScreen::increaseProgress (size_t increase) + { + mProgressBar->setScrollPosition(0); + size_t value = mProgress + increase; + mProgress = value; + assert(mProgress < mProgressBar->getScrollRange()); + mProgressBar->setTrackSize(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize()); + draw(); + } - mLoadingText->setCaption(stage); - mProgressBar->setProgressPosition (static_cast(progress * 1000)); + void LoadingScreen::indicateProgress() + { + float time = (mTimer.getMilliseconds() % 2001) / 1000.f; + if (time > 1) + time = (time-2)*-1; - static float loadingScreenFps = 30.f; + mProgressBar->setTrackSize(50); + mProgressBar->setScrollPosition(time * (mProgressBar->getScrollRange()-1)); + draw(); + } + + void LoadingScreen::removeWallpaper() + { + mFirstLoad = false; + } + + void LoadingScreen::draw() + { + const float loadingScreenFps = 20.f; if (mTimer.getMilliseconds () > mLastRenderTime + (1.f/loadingScreenFps) * 1000.f) { - float dt = mTimer.getMilliseconds () - mLastRenderTime; mLastRenderTime = mTimer.getMilliseconds (); - if (mFirstLoad && mTimer.getMilliseconds () > mLastWallpaperChangeTime + 3000*1) + if (mFirstLoad && mTimer.getMilliseconds () > mLastWallpaperChangeTime + 5000*1) { mLastWallpaperChangeTime = mTimer.getMilliseconds (); changeWallpaper(); @@ -129,8 +182,6 @@ namespace MWGui } mSceneMgr->setSpecialCaseRenderQueueMode(Ogre::SceneManager::SCRQM_EXCLUDE); - // always update input before rendering something, otherwise mygui goes crazy when something was entered in the frame before - // (e.g. when using "coc" console command, it would enter an infinite loop and crash due to overflow) MWBase::Environment::get().getInputManager()->update(0, true); Ogre::CompositorChain* chain = Ogre::CompositorManager::getSingleton().getCompositorChain(mWindow->getViewport(0)); @@ -156,9 +207,13 @@ namespace MWGui } } - MWBase::Environment::get().getWorld ()->getFader ()->update (dt); - - mWindow->update(); + // First, swap buffers from last draw, then, queue an update of the + // window contents, but don't swap buffers (which would have + // caused a sync / flush and would be expensive). + // We're doing this so we can do some actual loading while the GPU is busy with the render. + // This means the render is lagging a frame behind, but this is hardly noticable. + mWindow->swapBuffers(false); // never Vsync, makes no sense here + mWindow->update(false); if (!hasCompositor) mWindow->getViewport(0)->setClearEveryFrame(true); @@ -177,62 +232,4 @@ namespace MWGui mSceneMgr->setSpecialCaseRenderQueueMode(Ogre::SceneManager::SCRQM_EXCLUDE); } } - - void LoadingScreen::loadingDone() - { - loadingOff(); - } - - void LoadingScreen::loadingOn() - { - setVisible(true); - mLoadingOn = true; - - if (mFirstLoad) - { - changeWallpaper(); - - MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_LoadingWallpaper); - } - else - { - mBackgroundImage->setImageTexture(""); - MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Loading); - } - } - - - void LoadingScreen::loadingOff() - { - setVisible(false); - mLoadingOn = false; - mFirstLoad = false; - - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Loading); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_LoadingWallpaper); - } - - void LoadingScreen::changeWallpaper () - { - if (mResources.empty()) - { - Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); - for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) - { - Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "Splash_*.tga"); - mResources.insert(mResources.end(), resourcesInThisGroup->begin(), resourcesInThisGroup->end()); - } - } - - if (!mResources.empty()) - { - std::string const & randomSplash = mResources.at (rand() % mResources.size()); - - Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton ().load (randomSplash, Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME); - - mBackgroundImage->setImageTexture (randomSplash); - } - else - std::cerr << "No loading screens found!" << std::endl; - } } diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 87cedaa98..dde8ff63a 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -5,11 +5,27 @@ #include "windowbase.hpp" +#include + namespace MWGui { - class LoadingScreen : public WindowBase + class LoadingScreen : public WindowBase, public Loading::Listener { public: + virtual void setLabel (const std::string& label); + + /// Indicate that some progress has been made, without specifying how much + virtual void indicateProgress (); + + virtual void loadingOn(); + virtual void loadingOff(); + + virtual void setProgressRange (size_t range); + virtual void setProgress (size_t value); + virtual void increaseProgress (size_t increase); + + virtual void removeWallpaper(); + LoadingScreen(Ogre::SceneManager* sceneMgr, Ogre::RenderWindow* rw); virtual ~LoadingScreen(); @@ -30,27 +46,20 @@ namespace MWGui unsigned long mLastRenderTime; Ogre::Timer mTimer; + size_t mProgress; + MyGUI::TextBox* mLoadingText; - MyGUI::ProgressBar* mProgressBar; + MyGUI::ScrollBar* mProgressBar; MyGUI::ImageBox* mBackgroundImage; - int mCurrentCellLoading; - int mTotalCellsLoading; - int mCurrentRefLoading; - int mTotalRefsLoading; - int mCurrentRefList; - Ogre::Rectangle2D* mRectangle; Ogre::MaterialPtr mBackgroundMaterial; Ogre::StringVector mResources; - bool mLoadingOn; - - void loadingOn(); - void loadingOff(); - void changeWallpaper(); + + void draw(); }; } diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 0ccfb7e88..5ed002d7b 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -260,12 +260,10 @@ namespace MWGui MapWindow::MapWindow(const std::string& cacheDir) : MWGui::WindowPinnableBase("openmw_map_window.layout") , mGlobal(false) + , mGlobalMap(0) { setCoord(500,0,320,300); - mGlobalMapRender = new MWRender::GlobalMap(cacheDir); - mGlobalMapRender->render(); - getWidget(mLocalMap, "LocalMap"); getWidget(mGlobalMap, "GlobalMap"); getWidget(mGlobalMapImage, "GlobalMapImage"); @@ -273,9 +271,6 @@ namespace MWGui getWidget(mPlayerArrowLocal, "CompassLocal"); getWidget(mPlayerArrowGlobal, "CompassGlobal"); - mGlobalMapImage->setImageTexture("GlobalMap.png"); - mGlobalMapOverlay->setImageTexture("GlobalMapOverlay"); - mGlobalMap->setVisible (false); getWidget(mButton, "WorldButton"); @@ -292,6 +287,14 @@ namespace MWGui LocalMapBase::init(mLocalMap, mPlayerArrowLocal, this); } + void MapWindow::renderGlobalMap(Loading::Listener* loadingListener) + { + mGlobalMapRender = new MWRender::GlobalMap(""); + mGlobalMapRender->render(loadingListener); + mGlobalMapImage->setImageTexture("GlobalMap.png"); + mGlobalMapOverlay->setImageTexture("GlobalMapOverlay"); + } + MapWindow::~MapWindow() { delete mGlobalMapRender; diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 3aefc398c..5518ab4a8 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -8,6 +8,11 @@ namespace MWRender class GlobalMap; } +namespace Loading +{ + class Listener; +} + namespace MWGui { class LocalMapBase @@ -71,6 +76,8 @@ namespace MWGui void setCellName(const std::string& cellName); + void renderGlobalMap(Loading::Listener* loadingListener); + void addVisitedLocation(const std::string& name, int x, int y); // adds the marker to the global map void cellExplored(int x, int y); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 1cb1d80b0..ad1d9a60b 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -62,6 +62,7 @@ namespace MWGui const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding) : mGuiManager(NULL) + , mConsoleOnlyScripts(consoleOnlyScripts) , mRendering(ogre) , mHud(NULL) , mMap(NULL) @@ -156,7 +157,28 @@ namespace MWGui MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Get size info from the Gui object - assert(mGui); + int w = MyGUI::RenderManager::getInstance().getViewSize().width; + int h = MyGUI::RenderManager::getInstance().getViewSize().height; + + mLoadingScreen = new LoadingScreen(mRendering->getScene (), mRendering->getWindow ()); + mLoadingScreen->onResChange (w,h); + + //set up the hardware cursor manager + mSoftwareCursor = new Cursor(); + mCursorManager = new SFO::SDLCursorManager(); + + MyGUI::PointerManager::getInstance().eventChangeMousePointer += MyGUI::newDelegate(this, &WindowManager::onCursorChange); + + MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); + + setUseHardwareCursors(mUseHardwareCursors); + onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); + mCursorManager->cursorVisibilityChange(false); + } + + void WindowManager::initUI() + { + // Get size info from the Gui object int w = MyGUI::RenderManager::getInstance().getViewSize().width; int h = MyGUI::RenderManager::getInstance().getViewSize().height; @@ -169,9 +191,9 @@ namespace MWGui mDragAndDrop->mDragAndDropWidget = dragAndDropWidget; mMenu = new MainMenu(w,h); - mMap = new MapWindow(cacheDir); + mMap = new MapWindow(""); mStatsWindow = new StatsWindow(); - mConsole = new Console(w,h, consoleOnlyScripts); + mConsole = new Console(w,h, mConsoleOnlyScripts); mJournal = JournalWindow::create(JournalViewModel::create ()); mMessageBoxManager = new MessageBoxManager(); mInventoryWindow = new InventoryWindow(mDragAndDrop); @@ -200,13 +222,8 @@ namespace MWGui mSoulgemDialog = new SoulgemDialog(mMessageBoxManager); mCompanionWindow = new CompanionWindow(mDragAndDrop, mMessageBoxManager); - mLoadingScreen = new LoadingScreen(mRendering->getScene (), mRendering->getWindow ()); - mLoadingScreen->onResChange (w,h); - mInputBlocker = mGui->createWidget("",0,0,w,h,MyGUI::Align::Default,"Windows",""); - mSoftwareCursor = new Cursor(); - mHud->setVisible(mHudEnabled); mCharGen = new CharacterCreation(); @@ -225,19 +242,15 @@ namespace MWGui unsetSelectedSpell(); unsetSelectedWeapon(); - //set up the hardware cursor manager - mCursorManager = new SFO::SDLCursorManager(); - - MyGUI::PointerManager::getInstance().eventChangeMousePointer += MyGUI::newDelegate(this, &WindowManager::onCursorChange); - - MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); - - setUseHardwareCursors(mUseHardwareCursors); - onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); - mCursorManager->cursorVisibilityChange(false); - // Set up visibility updateVisible(); + + MWBase::Environment::get().getInputManager()->changeInputMode(false); + } + + void WindowManager::renderWorldMap() + { + mMap->renderGlobalMap(mLoadingScreen); } void WindowManager::setNewGame(bool newgame) @@ -329,6 +342,8 @@ namespace MWGui void WindowManager::updateVisible() { + if (!mMap) + return; // UI not created yet // Start out by hiding everything except the HUD mMap->setVisible(false); mMenu->setVisible(false); @@ -1148,7 +1163,7 @@ namespace MWGui bool WindowManager::isGuiMode() const { - return !mGuiModes.empty() || mMessageBoxManager->isInteractiveMessageBox(); + return !mGuiModes.empty() || (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); } bool WindowManager::isConsoleMode() const @@ -1211,7 +1226,8 @@ namespace MWGui void WindowManager::showCrosshair (bool show) { - mHud->setCrosshairVisible (show && mCrosshairEnabled); + if (mHud) + mHud->setCrosshairVisible (show && mCrosshairEnabled); } void WindowManager::activateQuickKey (int index) @@ -1230,15 +1246,6 @@ namespace MWGui mHud->setVisible (mHudEnabled); } - void WindowManager::setLoadingProgress (const std::string& stage, int depth, int current, int total) - { - mLoadingScreen->setLoadingProgress (stage, depth, current, total); - } - - void WindowManager::loadingDone () - { - mLoadingScreen->loadingDone (); - } bool WindowManager::getRestEnabled() { //Enable rest dialogue if character creation finished @@ -1345,4 +1352,9 @@ namespace MWGui mHud->setEnemy(enemy); } + Loading::Listener* WindowManager::getLoadingScreen() + { + return mLoadingScreen; + } + } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index e49b33647..badb333a7 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -91,6 +91,11 @@ namespace MWGui Translation::Storage& translationDataStorage, ToUTF8::FromType encoding); virtual ~WindowManager(); + void initUI(); + void renderWorldMap(); + + virtual Loading::Listener* getLoadingScreen(); + /** * Should be called each frame to update windows/gui elements. * This could mean updating sizes of gui elements or opening @@ -241,9 +246,6 @@ namespace MWGui virtual void executeInConsole (const std::string& path); - virtual void setLoadingProgress (const std::string& stage, int depth, int current, int total); - virtual void loadingDone(); - virtual void enableRest() { mRestAllowed = true; } virtual bool getRestEnabled(); @@ -275,6 +277,8 @@ namespace MWGui void onSoulgemDialogButtonPressed (int button); private: + bool mConsoleOnlyScripts; + OEngine::GUI::MyGUIManager *mGuiManager; OEngine::Render::OgreRenderer *mRendering; HUD *mHud; diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 573fe389c..1039a0dce 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -86,13 +86,10 @@ namespace namespace MWInput { InputManager::InputManager(OEngine::Render::OgreRenderer &ogre, - MWWorld::Player& player, - MWBase::WindowManager &windows, OMW::Engine& engine, const std::string& userFile, bool userFileExists) : mOgre(ogre) - , mPlayer(player) - , mWindows(windows) + , mPlayer(NULL) , mEngine(engine) , mMouseLookEnabled(true) , mMouseX(ogre.getWindow()->getWidth ()/2.f) @@ -124,8 +121,6 @@ namespace MWInput adjustMouseRegion (window->getWidth(), window->getHeight()); - MyGUI::InputManager::getInstance().injectMouseMove(mMouseX, mMouseY, 0); - loadKeyDefaults(); for (int i = 0; i < A_Last; ++i) @@ -140,8 +135,6 @@ namespace MWInput mControlSwitch["playermagic"] = true; mControlSwitch["playerviewswitch"] = true; mControlSwitch["vanitymode"] = true; - - changeInputMode(false); } InputManager::~InputManager() @@ -164,7 +157,7 @@ namespace MWInput if (action == A_Use) { - MWWorld::Class::get(mPlayer.getPlayer()).getCreatureStats(mPlayer.getPlayer()).setAttackingOrSpell(currentValue); + MWWorld::Class::get(mPlayer->getPlayer()).getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); if (currentValue == 1) { int type = MWMechanics::CreatureStats::AT_Chop; @@ -177,7 +170,7 @@ namespace MWInput if (forward && !side) type = MWMechanics::CreatureStats::AT_Thrust; - MWWorld::Class::get(mPlayer.getPlayer()).getCreatureStats(mPlayer.getPlayer()).setAttackType(type); + MWWorld::Class::get(mPlayer->getPlayer()).getCreatureStats(mPlayer->getPlayer()).setAttackType(type); } } @@ -204,9 +197,9 @@ namespace MWInput case A_Activate: resetIdleTime(); - if (mWindows.isGuiMode()) + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { - if (mWindows.getMode() == MWGui::GM_Container) + if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Container) toggleContainer (); else MWBase::Environment::get().getWindowManager()->activateKeyPressed(); @@ -266,7 +259,7 @@ namespace MWInput showQuickKeysMenu(); break; case A_ToggleHUD: - mWindows.toggleHud(); + MWBase::Environment::get().getWindowManager()->toggleHud(); break; } } @@ -274,8 +267,7 @@ namespace MWInput void InputManager::update(float dt, bool loading) { - // Tell OIS to handle all input events - mInputManager->capture(); + mInputManager->capture(loading); // inject some fake mouse movement to force updating MyGUI's widget states // this shouldn't do any harm since we're moving back to the original position afterwards MyGUI::InputManager::getInstance().injectMouseMove( int(mMouseX+1), int(mMouseY+1), mMouseWheel); @@ -285,18 +277,10 @@ namespace MWInput if (!loading) mInputBinder->update(dt); - // Update windows/gui as a result of input events - // For instance this could mean opening a new window/dialog, - // by doing this after the input events are handled we - // ensure that window/gui changes appear quickly while - // avoiding that window/gui changes does not happen in - // event callbacks (which may crash) - mWindows.update(); - - bool main_menu = mWindows.containsMode(MWGui::GM_MainMenu); + bool main_menu = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu); bool was_relative = mInputManager->getMouseRelative(); - bool is_relative = !mWindows.isGuiMode(); + bool is_relative = !MWBase::Environment::get().getWindowManager()->isGuiMode(); // don't keep the pointer away from the window edge in gui mode // stop using raw mouse motions and switch to system cursor movements @@ -312,8 +296,11 @@ namespace MWInput mInputManager->warpMouse(mMouseX, mMouseY); } + if (loading) + return; + // Disable movement in Gui mode - if (mWindows.isGuiMode()) return; + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Configure player movement according to keyboard input. Actual movement will @@ -324,45 +311,45 @@ namespace MWInput if (actionIsActive(A_MoveLeft)) { triedToMove = true; - mPlayer.setLeftRight (-1); + mPlayer->setLeftRight (-1); } else if (actionIsActive(A_MoveRight)) { triedToMove = true; - mPlayer.setLeftRight (1); + mPlayer->setLeftRight (1); } if (actionIsActive(A_MoveForward)) { triedToMove = true; - mPlayer.setAutoMove (false); - mPlayer.setForwardBackward (1); + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (1); } else if (actionIsActive(A_MoveBackward)) { triedToMove = true; - mPlayer.setAutoMove (false); - mPlayer.setForwardBackward (-1); + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (-1); } - else if(mPlayer.getAutoMove()) + else if(mPlayer->getAutoMove()) { triedToMove = true; - mPlayer.setForwardBackward (1); + mPlayer->setForwardBackward (1); } - mPlayer.setSneak(actionIsActive(A_Sneak)); + mPlayer->setSneak(actionIsActive(A_Sneak)); if (actionIsActive(A_Jump) && mControlSwitch["playerjumping"]) { - mPlayer.setUpDown (1); + mPlayer->setUpDown (1); triedToMove = true; } if (mAlwaysRunActive) - mPlayer.setRunState(!actionIsActive(A_Run)); + mPlayer->setRunState(!actionIsActive(A_Run)); else - mPlayer.setRunState(actionIsActive(A_Run)); + mPlayer->setRunState(actionIsActive(A_Run)); // if player tried to start moving, but can't (due to being overencumbered), display a notification. if (triedToMove) @@ -371,7 +358,7 @@ namespace MWInput mOverencumberedMessageDelay -= dt; if (MWWorld::Class::get(player).getEncumbrance(player) >= MWWorld::Class::get(player).getCapacity(player)) { - mPlayer.setAutoMove (false); + mPlayer->setAutoMove (false); if (mOverencumberedMessageDelay <= 0) { MWBase::Environment::get().getWindowManager ()->messageBox("#{sNotifyMessage59}"); @@ -425,8 +412,8 @@ namespace MWInput mGuiCursorEnabled = guiMode; mMouseLookEnabled = !guiMode; if (guiMode) - mWindows.showCrosshair(false); - mWindows.setCursorVisible(guiMode); + MWBase::Environment::get().getWindowManager()->showCrosshair(false); + MWBase::Environment::get().getWindowManager()->setCursorVisible(guiMode); // if not in gui mode, the camera decides whether to show crosshair or not. } @@ -459,13 +446,13 @@ namespace MWInput } /// \note 7 switches at all, if-else is relevant if (sw == "playercontrols" && !value) { - mPlayer.setLeftRight(0); - mPlayer.setForwardBackward(0); - mPlayer.setAutoMove(false); - mPlayer.setUpDown(0); + mPlayer->setLeftRight(0); + mPlayer->setForwardBackward(0); + mPlayer->setAutoMove(false); + mPlayer->setUpDown(0); } else if (sw == "playerjumping" && !value) { /// \fixme maybe crouching at this time - mPlayer.setUpDown(0); + mPlayer->setUpDown(0); } else if (sw == "vanitymode") { MWBase::Environment::get().getWorld()->allowVanityMode(value); } else if (sw == "playerlooking") { @@ -594,8 +581,8 @@ namespace MWInput // Only actually turn player when we're not in vanity mode if(!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot)) { - mPlayer.yaw(x/scale); - mPlayer.pitch(-y/scale); + mPlayer->yaw(x/scale); + mPlayer->pitch(-y/scale); } if (arg.zrel) @@ -627,51 +614,51 @@ namespace MWInput if (MyGUI::InputManager::getInstance ().isModalAny()) return; - if (mWindows.isGuiMode () && mWindows.getMode () == MWGui::GM_Video) + if (MWBase::Environment::get().getWindowManager()->isGuiMode () && MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_Video) MWBase::Environment::get().getWorld ()->stopVideo (); - else if (mWindows.containsMode(MWGui::GM_MainMenu)) - mWindows.popGuiMode(); + else if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu)) + MWBase::Environment::get().getWindowManager()->popGuiMode(); else - mWindows.pushGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); } void InputManager::toggleSpell() { - if (mWindows.isGuiMode()) return; + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the magic window is accessible - if (!mWindows.isAllowed(MWGui::GW_Magic)) + if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Magic)) return; - MWMechanics::DrawState_ state = mPlayer.getDrawState(); + MWMechanics::DrawState_ state = mPlayer->getDrawState(); if (state == MWMechanics::DrawState_Weapon || state == MWMechanics::DrawState_Nothing) - mPlayer.setDrawState(MWMechanics::DrawState_Spell); + mPlayer->setDrawState(MWMechanics::DrawState_Spell); else - mPlayer.setDrawState(MWMechanics::DrawState_Nothing); + mPlayer->setDrawState(MWMechanics::DrawState_Nothing); } void InputManager::toggleWeapon() { - if (mWindows.isGuiMode()) return; + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the inventory window is accessible - if (!mWindows.isAllowed(MWGui::GW_Inventory)) + if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; - MWMechanics::DrawState_ state = mPlayer.getDrawState(); + MWMechanics::DrawState_ state = mPlayer->getDrawState(); if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - mPlayer.setDrawState(MWMechanics::DrawState_Weapon); + mPlayer->setDrawState(MWMechanics::DrawState_Weapon); else - mPlayer.setDrawState(MWMechanics::DrawState_Nothing); + mPlayer->setDrawState(MWMechanics::DrawState_Nothing); } void InputManager::rest() { - if (!mWindows.getRestEnabled () || mWindows.isGuiMode ()) + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled () || MWBase::Environment::get().getWindowManager()->isGuiMode ()) return; /// \todo check if resting is currently allowed (enemies nearby?) - mWindows.pushGuiMode (MWGui::GM_Rest); + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_Rest); } void InputManager::screenshot() @@ -679,7 +666,7 @@ namespace MWInput mEngine.screenshot(); std::vector empty; - mWindows.messageBox ("Screenshot saved", empty); + MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved", empty); } void InputManager::toggleInventory() @@ -688,13 +675,13 @@ namespace MWInput return; // Toggle between game mode and inventory mode - if(!mWindows.isGuiMode()) - mWindows.pushGuiMode(MWGui::GM_Inventory); + if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Inventory); else { - MWGui::GuiMode mode = mWindows.getMode(); + MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); if(mode == MWGui::GM_Inventory || mode == MWGui::GM_Container) - mWindows.popGuiMode(); + MWBase::Environment::get().getWindowManager()->popGuiMode(); } // .. but don't touch any other mode, except container. @@ -705,12 +692,12 @@ namespace MWInput if (MyGUI::InputManager::getInstance ().isModalAny()) return; - if(mWindows.isGuiMode()) + if(MWBase::Environment::get().getWindowManager()->isGuiMode()) { - if (mWindows.getMode() == MWGui::GM_Container) - mWindows.popGuiMode(); + if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Container) + MWBase::Environment::get().getWindowManager()->popGuiMode(); else - mWindows.pushGuiMode(MWGui::GM_Container); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container); } } @@ -722,15 +709,15 @@ namespace MWInput // Switch to console mode no matter what mode we are currently // in, except of course if we are already in console mode - if (mWindows.isGuiMode()) + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { - if (mWindows.getMode() == MWGui::GM_Console) - mWindows.popGuiMode(); + if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Console) + MWBase::Environment::get().getWindowManager()->popGuiMode(); else - mWindows.pushGuiMode(MWGui::GM_Console); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Console); } else - mWindows.pushGuiMode(MWGui::GM_Console); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Console); } void InputManager::toggleJournal() @@ -739,31 +726,31 @@ namespace MWInput return; // Toggle between game mode and journal mode - if(!mWindows.isGuiMode() && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) + if(!MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) { MWBase::Environment::get().getSoundManager()->playSound ("book open", 1.0, 1.0); - mWindows.pushGuiMode(MWGui::GM_Journal); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); } - else if(mWindows.getMode() == MWGui::GM_Journal) + else if(MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Journal) { MWBase::Environment::get().getSoundManager()->playSound ("book close", 1.0, 1.0); - mWindows.popGuiMode(); + MWBase::Environment::get().getWindowManager()->popGuiMode(); } // .. but don't touch any other mode. } void InputManager::quickKey (int index) { - if (!mWindows.isGuiMode()) - mWindows.activateQuickKey (index); + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + MWBase::Environment::get().getWindowManager()->activateQuickKey (index); } void InputManager::showQuickKeysMenu() { - if (!mWindows.isGuiMode ()) - mWindows.pushGuiMode (MWGui::GM_QuickKeysMenu); - else if (mWindows.getMode () == MWGui::GM_QuickKeysMenu) - mWindows.removeGuiMode (MWGui::GM_QuickKeysMenu); + if (!MWBase::Environment::get().getWindowManager()->isGuiMode ()) + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_QuickKeysMenu); + else if (MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_QuickKeysMenu) + MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_QuickKeysMenu); } void InputManager::activate() @@ -774,22 +761,22 @@ namespace MWInput void InputManager::toggleAutoMove() { - if (mWindows.isGuiMode()) return; + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; if (mControlSwitch["playercontrols"]) - mPlayer.setAutoMove (!mPlayer.getAutoMove()); + mPlayer->setAutoMove (!mPlayer->getAutoMove()); } void InputManager::toggleWalking() { - if (mWindows.isGuiMode()) return; + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; mAlwaysRunActive = !mAlwaysRunActive; } // Exit program now button (which is disabled in GUI mode) void InputManager::exitNow() { - if(!mWindows.isGuiMode()) + if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) Ogre::Root::getSingleton().queueEndRendering (); } diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index ef7ef75a8..5f9a752d7 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -60,8 +60,6 @@ namespace MWInput { public: InputManager(OEngine::Render::OgreRenderer &_ogre, - MWWorld::Player&_player, - MWBase::WindowManager &_windows, OMW::Engine& engine, const std::string& userFile, bool userFileExists); @@ -69,6 +67,8 @@ namespace MWInput virtual void update(float dt, bool loading); + void setPlayer (MWWorld::Player* player) { mPlayer = player; } + virtual void changeInputMode(bool guiMode); virtual void processChangedSettings(const Settings::CategorySettingVector& changed); @@ -125,8 +125,7 @@ namespace MWInput private: OEngine::Render::OgreRenderer &mOgre; - MWWorld::Player& mPlayer; - MWBase::WindowManager &mWindows; + MWWorld::Player* mPlayer; OMW::Engine& mEngine; ICS::InputControlSystem* mInputBinder; diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 78b13ec07..120a83fae 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -28,7 +30,7 @@ namespace MWRender } - void GlobalMap::render () + void GlobalMap::render (Loading::Listener* loadingListener) { Ogre::TexturePtr tex; @@ -53,6 +55,11 @@ namespace MWRender mWidth = cellSize*(mMaxX-mMinX+1); mHeight = cellSize*(mMaxY-mMinY+1); + loadingListener->loadingOn(); + loadingListener->setLabel("Creating map"); + loadingListener->setProgressRange((mMaxX-mMinX+1) * (mMaxY-mMinY+1)); + loadingListener->setProgress(0); + mExploredBuffer.resize((mMaxX-mMinX+1) * (mMaxY-mMinY+1) * 4); //if (!boost::filesystem::exists(mCacheDir + "/GlobalMap.png")) @@ -147,6 +154,7 @@ namespace MWRender data[texelY * mWidth * 3 + texelX * 3+2] = b; } } + loadingListener->increaseProgress(1); } } @@ -177,6 +185,8 @@ namespace MWRender memcpy(mOverlayTexture->getBuffer()->lock(Ogre::HardwareBuffer::HBL_DISCARD), &buffer[0], mWidth*mHeight*4); mOverlayTexture->getBuffer()->unlock(); + + loadingListener->loadingOff(); } void GlobalMap::worldPosToImageSpace(float x, float z, float& imageX, float& imageY) diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index c01182c5b..dd3787b62 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -5,6 +5,11 @@ #include +namespace Loading +{ + class Listener; +} + namespace MWRender { @@ -13,7 +18,7 @@ namespace MWRender public: GlobalMap(const std::string& cacheDir); - void render(); + void render(Loading::Listener* loadingListener); int getWidth() { return mWidth; } int getHeight() { return mHeight; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index a7e35fa19..d6887fe6d 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -82,7 +82,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b Settings::Manager::setString("shader mode", "General", openGL ? (glES ? "glsles" : "glsl") : "hlsl"); } - mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 5); + mRendering.adjustCamera(Settings::Manager::getFloat("field of view", "General"), 5); mRendering.getWindow()->addListener(this); mRendering.setWindowListener(this); @@ -1018,12 +1018,15 @@ void RenderingManager::enableTerrain(bool enable) { if (!mTerrain) { - mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + Loading::ScopedLoad load(listener); + mTerrain = new Terrain::Terrain(listener, mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, Settings::Manager::getBool("distant land", "Terrain"), Settings::Manager::getBool("shader", "Terrain")); mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), Settings::Manager::getBool("split", "Shadows")); mTerrain->update(mRendering.getCamera()->getRealPosition()); + mTerrain->setLoadingListener(NULL); } mTerrain->setVisible(true); } diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 5a4f1db54..7703f2d23 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -5,6 +5,8 @@ #include +#include + namespace MWWorld { @@ -21,8 +23,10 @@ static bool isCacheableRecord(int id) return false; } -void ESMStore::load(ESM::ESMReader &esm) +void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) { + listener->setProgressRange(1000); + std::set missing; ESM::Dialogue *dialogue = 0; @@ -109,6 +113,7 @@ void ESMStore::load(ESM::ESMReader &esm) mIds[id] = n.val; } } + listener->setProgress(esm.getFileOffset() / (float)esm.getFileSize() * 1000); } /* This information isn't needed on screen. But keep the code around diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index d86faf766..ebb086cee 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -6,6 +6,11 @@ #include #include "store.hpp" +namespace Loading +{ + class Listener; +} + namespace MWWorld { class ESMStore @@ -158,7 +163,7 @@ namespace MWWorld mNpcs.insert(mPlayerTemplate); } - void load(ESM::ESMReader &esm); + void load(ESM::ESMReader &esm, Loading::Listener* listener); template const Store &get() const { diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index ee8973ae5..0c98ca504 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -25,13 +25,12 @@ namespace template void insertCellRefList(MWRender::RenderingManager& rendering, - T& cellRefList, MWWorld::CellStore &cell, MWWorld::PhysicsSystem& physics, bool rescale) + T& cellRefList, MWWorld::CellStore &cell, MWWorld::PhysicsSystem& physics, bool rescale, Loading::Listener* loadingListener) { if (!cellRefList.mList.empty()) { const MWWorld::Class& class_ = MWWorld::Class::get (MWWorld::Ptr (&*cellRefList.mList.begin(), &cell)); - int current = 0; for (typename T::List::iterator it = cellRefList.mList.begin(); it != cellRefList.mList.end(); it++) { @@ -43,8 +42,6 @@ namespace it->mRef.mScale = 2; } - ++current; - if (it->mData.getCount() && it->mData.isEnabled()) { MWWorld::Ptr ptr (&*it, &cell); @@ -68,6 +65,8 @@ namespace std::cerr << error + e.what() << std::endl; } } + + loadingListener->increaseProgress(1); } } } @@ -117,7 +116,7 @@ namespace MWWorld mActiveCells.erase(*iter); } - void Scene::loadCell (Ptr::CellStore *cell) + void Scene::loadCell (Ptr::CellStore *cell, Loading::Listener* loadingListener) { // register local scripts MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); @@ -150,7 +149,7 @@ namespace MWWorld // ... then references. This is important for adjustPosition to work correctly. /// \todo rescale depending on the state of a new GMST - insertCell (*cell, true); + insertCell (*cell, true, loadingListener); mRendering.cellAdded (cell); @@ -200,17 +199,15 @@ namespace MWWorld void Scene::changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos) { Nif::NIFFile::CacheLock cachelock; - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - mRendering.preCellChange(mCurrentCell); + Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + Loading::ScopedLoad load(loadingListener); // remove active MWBase::Environment::get().getMechanicsManager()->remove(MWBase::Environment::get().getWorld()->getPlayer().getPlayer()); - std::string loadingExteriorText; - - loadingExteriorText = gmst.find ("sLoadingMessage3")->getString(); + std::string loadingExteriorText = "#{sLoadingMessage3}"; + loadingListener->setLabel(loadingExteriorText); CellStoreCollection::iterator active = mActiveCells.begin(); @@ -232,7 +229,6 @@ namespace MWWorld ++numUnload; } - int current = 0; active = mActiveCells.begin(); while (active!=mActiveCells.end()) { @@ -247,11 +243,10 @@ namespace MWWorld } } unloadCell (active++); - ++current; } - int numLoad = 0; - // get the number of cells to load + int refsToLoad = 0; + // get the number of refs to load for (int x=X-1; x<=X+1; ++x) for (int y=Y-1; y<=Y+1; ++y) { @@ -269,11 +264,12 @@ namespace MWWorld } if (iter==mActiveCells.end()) - ++numLoad; + refsToLoad += countRefs(*MWBase::Environment::get().getWorld()->getExterior(x, y)); } + loadingListener->setProgressRange(refsToLoad); + // Load cells - current = 0; for (int x=X-1; x<=X+1; ++x) for (int y=Y-1; y<=Y+1; ++y) { @@ -294,11 +290,7 @@ namespace MWWorld { CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - //Loading Exterior loading text - MWBase::Environment::get().getWindowManager ()->setLoadingProgress (loadingExteriorText, 0, current, numLoad); - - loadCell (cell); - ++current; + loadCell (cell, loadingListener); } } @@ -330,7 +322,7 @@ namespace MWWorld mCellChanged = true; - MWBase::Environment::get().getWindowManager ()->loadingDone (); + loadingListener->removeWallpaper(); } //We need the ogre renderer and a scene node. @@ -356,13 +348,14 @@ namespace MWWorld void Scene::changeToInteriorCell (const std::string& cellName, const ESM::Position& position) { MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(0.5); - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); mRendering.enableTerrain(false); - std::string loadingInteriorText; - loadingInteriorText = gmst.find ("sLoadingMessage2")->getString(); + Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + Loading::ScopedLoad load(loadingListener); + + std::string loadingInteriorText = "#{sLoadingMessage2}"; + loadingListener->setLabel(loadingInteriorText); CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); bool loadcell = (mCurrentCell == NULL); @@ -406,13 +399,15 @@ namespace MWWorld ++current; } + int refsToLoad = countRefs(*cell); + loadingListener->setProgressRange(refsToLoad); + // Load cell. std::cout << "cellName: " << cell->mCell->mName << std::endl; //Loading Interior loading text - MWBase::Environment::get().getWindowManager ()->setLoadingProgress (loadingInteriorText, 0, 0, 1); - loadCell (cell); + loadCell (cell, loadingListener); mCurrentCell = cell; @@ -429,7 +424,7 @@ namespace MWWorld mCellChanged = true; MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(0.5); - MWBase::Environment::get().getWindowManager ()->loadingDone (); + loadingListener->removeWallpaper(); } void Scene::changeToExteriorCell (const ESM::Position& position) @@ -454,30 +449,54 @@ namespace MWWorld mCellChanged = false; } - void Scene::insertCell (Ptr::CellStore &cell, bool rescale) + int Scene::countRefs (const Ptr::CellStore& cell) + { + return cell.mActivators.mList.size() + + cell.mPotions.mList.size() + + cell.mAppas.mList.size() + + cell.mArmors.mList.size() + + cell.mBooks.mList.size() + + cell.mClothes.mList.size() + + cell.mContainers.mList.size() + + cell.mDoors.mList.size() + + cell.mIngreds.mList.size() + + cell.mCreatureLists.mList.size() + + cell.mItemLists.mList.size() + + cell.mLights.mList.size() + + cell.mLockpicks.mList.size() + + cell.mMiscItems.mList.size() + + cell.mProbes.mList.size() + + cell.mRepairs.mList.size() + + cell.mStatics.mList.size() + + cell.mWeapons.mList.size() + + cell.mCreatures.mList.size() + + cell.mNpcs.mList.size(); + } + + void Scene::insertCell (Ptr::CellStore &cell, bool rescale, Loading::Listener* loadingListener) { // Loop through all references in the cell - insertCellRefList(mRendering, cell.mActivators, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mPotions, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mAppas, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mArmors, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mBooks, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mClothes, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mContainers, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mDoors, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mIngreds, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mCreatureLists, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mItemLists, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mLights, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mLockpicks, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mMiscItems, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mProbes, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mRepairs, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mStatics, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mWeapons, cell, *mPhysics, rescale); + insertCellRefList(mRendering, cell.mActivators, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mPotions, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mAppas, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mArmors, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mBooks, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mClothes, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mContainers, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mDoors, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mIngreds, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mCreatureLists, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mItemLists, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mLights, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mLockpicks, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mMiscItems, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mProbes, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mRepairs, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mStatics, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mWeapons, cell, *mPhysics, rescale, loadingListener); // Load NPCs and creatures _after_ everything else (important for adjustPosition to work correctly) - insertCellRefList(mRendering, cell.mCreatures, cell, *mPhysics, rescale); - insertCellRefList(mRendering, cell.mNpcs, cell, *mPhysics, rescale); + insertCellRefList(mRendering, cell.mCreatures, cell, *mPhysics, rescale, loadingListener); + insertCellRefList(mRendering, cell.mNpcs, cell, *mPhysics, rescale, loadingListener); } void Scene::addObjectToScene (const Ptr& ptr) diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 3dfbd1045..e3edad352 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -56,7 +56,9 @@ namespace MWWorld void playerCellChange (CellStore *cell, const ESM::Position& position, bool adjustPlayerPos = true); - void insertCell (Ptr::CellStore &cell, bool rescale); + void insertCell (Ptr::CellStore &cell, bool rescale, Loading::Listener* loadingListener); + + int countRefs (const Ptr::CellStore& cell); public: @@ -66,7 +68,7 @@ namespace MWWorld void unloadCell (CellStoreCollection::iterator iter); - void loadCell (CellStore *cell); + void loadCell (CellStore *cell, Loading::Listener* loadingListener); void changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos); ///< Move from exterior to interior or from interior cell to a different diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 09dc0493e..eac0a3977 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -184,11 +184,14 @@ namespace MWWorld int idx = 0; // NOTE: We might need to reserve one more for the running game / save. mEsm.resize(master.size() + plugins.size()); + Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + listener->loadingOn(); for (std::vector::size_type i = 0; i < master.size(); i++, idx++) { boost::filesystem::path masterPath (fileCollections.getCollection (".esm").getPath (master[i])); std::cout << "Loading ESM " << masterPath.string() << "\n"; + listener->setLabel(masterPath.filename().string()); // This parses the ESM file ESM::ESMReader lEsm; @@ -197,7 +200,7 @@ namespace MWWorld lEsm.setGlobalReaderList(&mEsm); lEsm.open (masterPath.string()); mEsm[idx] = lEsm; - mStore.load (mEsm[idx]); + mStore.load (mEsm[idx], listener); } for (std::vector::size_type i = 0; i < plugins.size(); i++, idx++) @@ -205,6 +208,7 @@ namespace MWWorld boost::filesystem::path pluginPath (fileCollections.getCollection (".esp").getPath (plugins[i])); std::cout << "Loading ESP " << pluginPath.string() << "\n"; + listener->setLabel(pluginPath.filename().string()); // This parses the ESP file ESM::ESMReader lEsm; @@ -213,8 +217,9 @@ namespace MWWorld lEsm.setGlobalReaderList(&mEsm); lEsm.open (pluginPath.string()); mEsm[idx] = lEsm; - mStore.load (mEsm[idx]); + mStore.load (mEsm[idx], listener); } + listener->loadingOff(); // insert records that may not be present in all versions of MW if (mEsm[0].getFormat() == 0) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index baf905aa7..1dd6816ac 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -69,6 +69,10 @@ add_component_dir (translation add_component_dir (terrain quadtreenode chunk terrain storage material ) + +add_component_dir (loadinglistener + loadinglistener + ) find_package(Qt4 COMPONENTS QtCore QtGui) diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index ff10a202c..edc724cd2 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -70,6 +70,11 @@ public: void openRaw(const std::string &file); + /// Get the file size. Make sure that the file has been opened! + size_t getFileSize() { return mEsm->size(); } + /// Get the current position in the file. Make sure that the file has been opened! + size_t getFileOffset() { return mEsm->tell(); } + // This is a quick hack for multiple esm/esp files. Each plugin introduces its own // terrain palette, but ESMReader does not pass a reference to the correct plugin // to the individual load() methods. This hack allows to pass this reference diff --git a/components/loadinglistener/loadinglistener.hpp b/components/loadinglistener/loadinglistener.hpp new file mode 100644 index 000000000..483d52491 --- /dev/null +++ b/components/loadinglistener/loadinglistener.hpp @@ -0,0 +1,35 @@ +#ifndef COMPONENTS_LOADINGLISTENER_H +#define COMPONENTS_LOADINGLISTENER_H + +namespace Loading +{ + class Listener + { + public: + virtual void setLabel (const std::string& label) = 0; + + // Use ScopedLoad instead of using these directly + virtual void loadingOn() = 0; + virtual void loadingOff() = 0; + + /// Indicate that some progress has been made, without specifying how much + virtual void indicateProgress () = 0; + + virtual void setProgressRange (size_t range) = 0; + virtual void setProgress (size_t value) = 0; + virtual void increaseProgress (size_t increase) = 0; + + /// Indicate the scene is now ready to be shown + virtual void removeWallpaper() = 0; + }; + + // Used for stopping a loading sequence when the object goes out of scope + struct ScopedLoad + { + ScopedLoad(Listener* l) : mListener(l) { mListener->loadingOn(); } + ~ScopedLoad() { mListener->loadingOff(); } + Listener* mListener; + }; +} + +#endif diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index ed5673877..e581c40ea 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -220,7 +220,7 @@ const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox() return mBounds; } -void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) +void QuadTreeNode::update(const Ogre::Vector3 &cameraPos, Loading::Listener* loadingListener) { const Ogre::AxisAlignedBox& bounds = getBoundingBox(); if (bounds.isNull()) @@ -254,6 +254,9 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) bool hadChunk = hasChunk(); + if (loadingListener) + loadingListener->indicateProgress(); + if (!distantLand && dist > 8192*2) { if (mIsActive) @@ -341,7 +344,7 @@ void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) } assert(hasChildren() && "Leaf node's LOD needs to be 0"); for (int i=0; i<4; ++i) - mChildren[i]->update(cameraPos); + mChildren[i]->update(cameraPos, loadingListener); } } diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index fe646f3bf..5adb9676a 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -5,6 +5,8 @@ #include #include +#include + namespace Ogre { class Rectangle2D; @@ -96,7 +98,7 @@ namespace Terrain Terrain* getTerrain() { return mTerrain; } /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. - void update (const Ogre::Vector3& cameraPos); + void update (const Ogre::Vector3& cameraPos, Loading::Listener* loadingListener); /// Adjust index buffers of chunks to stitch together chunks of different LOD, so that cracks are avoided. /// Call after QuadTreeNode::update! diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp index 1a88d63e4..31a6ff526 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/terrain.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "storage.hpp" #include "quadtreenode.hpp" @@ -51,7 +52,8 @@ namespace namespace Terrain { - Terrain::Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags, bool distantLand, bool shaders) + Terrain::Terrain(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, + Storage* storage, int visibilityFlags, bool distantLand, bool shaders) : mStorage(storage) , mMinBatchSize(1) , mMaxBatchSize(64) @@ -60,7 +62,10 @@ namespace Terrain , mDistantLand(distantLand) , mShaders(shaders) , mVisible(true) + , mLoadingListener(loadingListener) { + loadingListener->setLabel("Creating terrain"); + mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); Ogre::Camera* compositeMapCam = mCompositeMapSceneMgr->createCamera("a"); @@ -86,8 +91,11 @@ namespace Terrain mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(center.x, center.y), NULL); buildQuadTree(mRootNode); + loadingListener->indicateProgress(); mRootNode->initAabb(); + loadingListener->indicateProgress(); mRootNode->initNeighbours(); + loadingListener->indicateProgress(); } Terrain::~Terrain() @@ -147,7 +155,7 @@ namespace Terrain { if (!mVisible) return; - mRootNode->update(cameraPos); + mRootNode->update(cameraPos, mLoadingListener); mRootNode->updateIndexBuffers(); } diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp index 4d329f9e1..ad829cb47 100644 --- a/components/terrain/terrain.hpp +++ b/components/terrain/terrain.hpp @@ -6,6 +6,11 @@ #include #include +namespace Loading +{ + class Listener; +} + namespace Ogre { class Camera; @@ -28,6 +33,7 @@ namespace Terrain { public: /// @note takes ownership of \a storage + /// @param loadingListener Listener to update with progress /// @param sceneMgr scene manager to use /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param visbilityFlags visibility flags for the created meshes @@ -35,9 +41,12 @@ namespace Terrain /// This is a temporary option until it can be streamlined. /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually /// faster so this is just here for compatibility. - Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags, bool distantLand, bool shaders); + Terrain(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, + Storage* storage, int visiblityFlags, bool distantLand, bool shaders); ~Terrain(); + void setLoadingListener(Loading::Listener* loadingListener) { mLoadingListener = loadingListener; } + bool getDistantLandEnabled() { return mDistantLand; } bool getShadersEnabled() { return mShaders; } bool getShadowsEnabled() { return mShadows; } @@ -84,6 +93,8 @@ namespace Terrain bool mSplitShadows; bool mVisible; + Loading::Listener* mLoadingListener; + QuadTreeNode* mRootNode; Ogre::SceneNode* mRootSceneNode; Storage* mStorage; diff --git a/extern/sdl4ogre/sdlinputwrapper.cpp b/extern/sdl4ogre/sdlinputwrapper.cpp index 5602946ff..931d6aca3 100644 --- a/extern/sdl4ogre/sdlinputwrapper.cpp +++ b/extern/sdl4ogre/sdlinputwrapper.cpp @@ -35,9 +35,20 @@ namespace SFO mSDLWindow = NULL; } - void InputWrapper::capture() + void InputWrapper::capture(bool windowEventsOnly) { + SDL_PumpEvents(); + SDL_Event evt; + + if (windowEventsOnly) + { + // During loading, just handle window events, and keep others for later + while (SDL_PeepEvents(&evt, 1, SDL_GETEVENT, SDL_WINDOWEVENT, SDL_WINDOWEVENT)) + handleWindowEvent(evt); + return; + } + while(SDL_PollEvent(&evt)) { switch(evt.type) diff --git a/extern/sdl4ogre/sdlinputwrapper.hpp b/extern/sdl4ogre/sdlinputwrapper.hpp index 66bf60c7c..1bd8947a0 100644 --- a/extern/sdl4ogre/sdlinputwrapper.hpp +++ b/extern/sdl4ogre/sdlinputwrapper.hpp @@ -24,7 +24,7 @@ namespace SFO void setWindowEventCallback(WindowListener* listen) { mWindowListener = listen; } void setJoyEventCallback(JoyListener* listen) { mJoyListener = listen; } - void capture(); + void capture(bool windowEventsOnly); bool isModifierHeld(SDL_Keymod mod); bool isKeyDown(SDL_Scancode key); diff --git a/files/mygui/openmw_loading_screen.layout b/files/mygui/openmw_loading_screen.layout index 1e4bba5ed..5fd3440f9 100644 --- a/files/mygui/openmw_loading_screen.layout +++ b/files/mygui/openmw_loading_screen.layout @@ -12,8 +12,7 @@ - - + diff --git a/files/mygui/openmw_progress.skin.xml b/files/mygui/openmw_progress.skin.xml index aa994fea7..35114ffeb 100644 --- a/files/mygui/openmw_progress.skin.xml +++ b/files/mygui/openmw_progress.skin.xml @@ -58,10 +58,23 @@ - + + + + + + + + + + + + + + diff --git a/libs/openengine/ogre/renderer.cpp b/libs/openengine/ogre/renderer.cpp index 0834a2cd1..912781240 100644 --- a/libs/openengine/ogre/renderer.cpp +++ b/libs/openengine/ogre/renderer.cpp @@ -279,27 +279,23 @@ void OgreRenderer::createWindow(const std::string &title, const WindowSettings& 0, Ogre::PF_A8R8G8B8, Ogre::TU_WRITE_ONLY); -} -void OgreRenderer::createScene(const std::string& camName, float fov, float nearClip) -{ - assert(mRoot); - assert(mWindow); - // Get the SceneManager, in this case a generic one mScene = mRoot->createSceneManager(ST_GENERIC); - // Create the camera - mCamera = mScene->createCamera(camName); - mCamera->setNearClipDistance(nearClip); - mCamera->setFOVy(Degree(fov)); + mFader = new Fader(mScene); + + mCamera = mScene->createCamera("cam"); // Create one viewport, entire window mView = mWindow->addViewport(mCamera); - // Alter the camera aspect ratio to match the viewport mCamera->setAspectRatio(Real(mView->getActualWidth()) / Real(mView->getActualHeight())); +} - mFader = new Fader(mScene); +void OgreRenderer::adjustCamera(float fov, float nearClip) +{ + mCamera->setNearClipDistance(nearClip); + mCamera->setFOVy(Degree(fov)); } void OgreRenderer::adjustViewport() diff --git a/libs/openengine/ogre/renderer.hpp b/libs/openengine/ogre/renderer.hpp index 9e83bf4f6..89edc567d 100644 --- a/libs/openengine/ogre/renderer.hpp +++ b/libs/openengine/ogre/renderer.hpp @@ -157,7 +157,7 @@ namespace OEngine void createWindow(const std::string &title, const WindowSettings& settings); /// Set up the scene manager, camera and viewport - void createScene(const std::string& camName="Camera",// Camera name + void adjustCamera( float fov=55, // Field of view angle float nearClip=5 // Near clip distance ); From 839d251cc5b763e89ca0f5dc23a05bcf65a05885 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 27 Aug 2013 16:01:16 +0200 Subject: [PATCH 121/194] Renamed Terrain::Terrain to Terrain::World to make VC happy --- apps/openmw/mwrender/renderingmanager.cpp | 4 +-- apps/openmw/mwrender/renderingmanager.hpp | 4 +-- components/CMakeLists.txt | 2 +- components/terrain/chunk.cpp | 2 +- components/terrain/quadtreenode.cpp | 4 +-- components/terrain/quadtreenode.hpp | 8 ++--- components/terrain/{terrain.cpp => world.cpp} | 29 ++++++++++--------- components/terrain/{terrain.hpp => world.hpp} | 6 ++-- 8 files changed, 30 insertions(+), 29 deletions(-) rename components/terrain/{terrain.cpp => world.cpp} (94%) rename components/terrain/{terrain.hpp => world.hpp} (97%) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index d6887fe6d..e03b2ccfc 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -25,7 +25,7 @@ #include #include -#include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -1020,7 +1020,7 @@ void RenderingManager::enableTerrain(bool enable) { Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(listener); - mTerrain = new Terrain::Terrain(listener, mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + mTerrain = new Terrain::World(listener, mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, Settings::Manager::getBool("distant land", "Terrain"), Settings::Manager::getBool("shader", "Terrain")); mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 45929f064..2d0813912 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -40,7 +40,7 @@ namespace sh namespace Terrain { - class Terrain; + class World; } namespace MWRender @@ -234,7 +234,7 @@ private: OcclusionQuery* mOcclusionQuery; - Terrain::Terrain* mTerrain; + Terrain::World* mTerrain; MWRender::Water *mWater; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 1dd6816ac..04423dc6f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -67,7 +67,7 @@ add_component_dir (translation ) add_component_dir (terrain - quadtreenode chunk terrain storage material + quadtreenode chunk world storage material ) add_component_dir (loadinglistener diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp index 87222af8b..ce2118cdb 100644 --- a/components/terrain/chunk.cpp +++ b/components/terrain/chunk.cpp @@ -4,7 +4,7 @@ #include #include "quadtreenode.hpp" -#include "terrain.hpp" +#include "world.hpp" #include "storage.hpp" namespace Terrain diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index e581c40ea..95daa0afb 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -3,7 +3,7 @@ #include #include -#include "terrain.hpp" +#include "world.hpp" #include "chunk.hpp" #include "storage.hpp" @@ -132,7 +132,7 @@ namespace } } -QuadTreeNode::QuadTreeNode(Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) +QuadTreeNode::QuadTreeNode(World* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) : mSize(size) , mCenter(center) , mParent(parent) diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index 5adb9676a..9f6fb5d24 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -14,7 +14,7 @@ namespace Ogre namespace Terrain { - class Terrain; + class World; class Chunk; class MaterialGenerator; @@ -48,7 +48,7 @@ namespace Terrain /// @param size size (in *cell* units!) /// @param center center (in *cell* units!) /// @param parent parent node - QuadTreeNode (Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); + QuadTreeNode (World* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); ~QuadTreeNode(); void setVisible(bool visible); @@ -95,7 +95,7 @@ namespace Terrain /// Get bounding box in local coordinates const Ogre::AxisAlignedBox& getBoundingBox(); - Terrain* getTerrain() { return mTerrain; } + World* getTerrain() { return mTerrain; } /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. void update (const Ogre::Vector3& cameraPos, Loading::Listener* loadingListener); @@ -148,7 +148,7 @@ namespace Terrain Chunk* mChunk; - Terrain* mTerrain; + World* mTerrain; Ogre::TexturePtr mCompositeMap; diff --git a/components/terrain/terrain.cpp b/components/terrain/world.cpp similarity index 94% rename from components/terrain/terrain.cpp rename to components/terrain/world.cpp index 31a6ff526..711ebbc8f 100644 --- a/components/terrain/terrain.cpp +++ b/components/terrain/world.cpp @@ -1,4 +1,4 @@ -#include "terrain.hpp" +#include "world.hpp" #include #include @@ -52,7 +52,7 @@ namespace namespace Terrain { - Terrain::Terrain(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, + World::World(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags, bool distantLand, bool shaders) : mStorage(storage) , mMinBatchSize(1) @@ -65,6 +65,7 @@ namespace Terrain , mLoadingListener(loadingListener) { loadingListener->setLabel("Creating terrain"); + loadingListener->indicateProgress(); mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); @@ -98,13 +99,13 @@ namespace Terrain loadingListener->indicateProgress(); } - Terrain::~Terrain() + World::~World() { delete mRootNode; delete mStorage; } - void Terrain::buildQuadTree(QuadTreeNode *node) + void World::buildQuadTree(QuadTreeNode *node) { float halfSize = node->getSize()/2.f; @@ -151,7 +152,7 @@ namespace Terrain node->markAsDummy(); } - void Terrain::update(const Ogre::Vector3& cameraPos) + void World::update(const Ogre::Vector3& cameraPos) { if (!mVisible) return; @@ -159,7 +160,7 @@ namespace Terrain mRootNode->updateIndexBuffers(); } - Ogre::AxisAlignedBox Terrain::getWorldBoundingBox (const Ogre::Vector2& center) + Ogre::AxisAlignedBox World::getWorldBoundingBox (const Ogre::Vector2& center) { if (center.x > mBounds.getMaximum().x || center.x < mBounds.getMinimum().x @@ -173,7 +174,7 @@ namespace Terrain return box; } - Ogre::HardwareVertexBufferSharedPtr Terrain::getVertexBuffer(int numVertsOneSide) + Ogre::HardwareVertexBufferSharedPtr World::getVertexBuffer(int numVertsOneSide) { if (mUvBufferMap.find(numVertsOneSide) != mUvBufferMap.end()) { @@ -205,7 +206,7 @@ namespace Terrain return buffer; } - Ogre::HardwareIndexBufferSharedPtr Terrain::getIndexBuffer(int flags, size_t& numIndices) + Ogre::HardwareIndexBufferSharedPtr World::getIndexBuffer(int flags, size_t& numIndices) { if (mIndexBufferMap.find(flags) != mIndexBufferMap.end()) { @@ -366,31 +367,31 @@ namespace Terrain return buffer; } - void Terrain::renderCompositeMap(Ogre::TexturePtr target) + void World::renderCompositeMap(Ogre::TexturePtr target) { mCompositeMapRenderTarget->update(); target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer()); } - void Terrain::clearCompositeMapSceneManager() + void World::clearCompositeMapSceneManager() { mCompositeMapSceneMgr->destroyAllManualObjects(); mCompositeMapSceneMgr->clearScene(); } - float Terrain::getHeightAt(const Ogre::Vector3 &worldPos) + float World::getHeightAt(const Ogre::Vector3 &worldPos) { return mStorage->getHeightAt(worldPos); } - void Terrain::applyMaterials(bool shadows, bool splitShadows) + void World::applyMaterials(bool shadows, bool splitShadows) { mShadows = shadows; mSplitShadows = splitShadows; mRootNode->applyMaterials(); } - void Terrain::setVisible(bool visible) + void World::setVisible(bool visible) { if (visible && !mVisible) mSceneMgr->getRootSceneNode()->addChild(mRootSceneNode); @@ -400,7 +401,7 @@ namespace Terrain mVisible = visible; } - bool Terrain::getVisible() + bool World::getVisible() { return mVisible; } diff --git a/components/terrain/terrain.hpp b/components/terrain/world.hpp similarity index 97% rename from components/terrain/terrain.hpp rename to components/terrain/world.hpp index ad829cb47..b8c1b0a7d 100644 --- a/components/terrain/terrain.hpp +++ b/components/terrain/world.hpp @@ -29,7 +29,7 @@ namespace Terrain * Cracks at LOD transitions are avoided using stitching. * @note Multiple cameras are not supported yet */ - class Terrain + class World { public: /// @note takes ownership of \a storage @@ -41,9 +41,9 @@ namespace Terrain /// This is a temporary option until it can be streamlined. /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually /// faster so this is just here for compatibility. - Terrain(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, + World(Loading::Listener* loadingListener, Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags, bool distantLand, bool shaders); - ~Terrain(); + ~World(); void setLoadingListener(Loading::Listener* loadingListener) { mLoadingListener = loadingListener; } From 188df341c3e403052557d771125f07a314afd51d Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 27 Aug 2013 16:08:58 +0200 Subject: [PATCH 122/194] Replaced log2 to make VC happy --- components/terrain/quadtreenode.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 95daa0afb..ef2c61013 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -13,6 +13,13 @@ using namespace Terrain; namespace { + int Log2( int n ) + { + assert(n > 0); + int targetlevel = 0; + while (n >>= 1) ++targetlevel; + return targetlevel; + } // Utility functions for neighbour finding algorithm ChildDirection reflect(ChildDirection dir, Direction dir2) @@ -161,7 +168,7 @@ QuadTreeNode::QuadTreeNode(World* terrain, ChildDirection dir, float size, const pos = mCenter - pos; mSceneNode->setPosition(Ogre::Vector3(pos.x*8192, pos.y*8192, 0)); - mLodLevel = log2(mSize); + mLodLevel = Log2(mSize); mMaterialGenerator = new MaterialGenerator(mTerrain->getShadersEnabled()); } From e5ce94c33604b51afc2e1ecfdbe4d269e1ba97f5 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 27 Aug 2013 16:45:51 +0200 Subject: [PATCH 123/194] Fix the water being affected by fog on the map --- files/materials/water.shader | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/files/materials/water.shader b/files/materials/water.shader index a6d0a47e6..87e90a291 100644 --- a/files/materials/water.shader +++ b/files/materials/water.shader @@ -3,23 +3,28 @@ #define SIMPLE_WATER @shGlobalSettingBool(simple_water) - #if SIMPLE_WATER // --------------------------------------- SIMPLE WATER --------------------------------------------------- +#define FOG @shGlobalSettingBool(fog) + #ifdef SH_VERTEX_SHADER SH_BEGIN_PROGRAM shUniform(float4x4, wvp) @shAutoConstant(wvp, worldviewproj_matrix) shVertexInput(float2, uv0) shOutput(float2, UV) - shOutput(float, depth) +#if FOG + shOutput(float, depth) +#endif SH_START_PROGRAM { shOutputPosition = shMatrixMult(wvp, shInputPosition); UV = uv0; +#if FOG depth = shOutputPosition.z; +#endif } #else @@ -38,8 +43,10 @@ shOutputColour(0).xyz = shSample(animatedTexture, UV * 15).xyz * float3(1.0, 1.0, 1.0); shOutputColour(0).w = 0.7; +#if FOG float fogValue = shSaturate((depth - fogParams.y) * fogParams.w); shOutputColour(0).xyz = shLerp (shOutputColour(0).xyz, fogColor, fogValue); +#endif } #endif From 26b3d93293afc4363fa9e715c217bc8e3dbf5ca5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 27 Aug 2013 18:58:23 +0200 Subject: [PATCH 124/194] streamlined filter syntax --- apps/opencs/model/filter/parser.cpp | 68 +++++++++++++++-------------- apps/opencs/model/filter/parser.hpp | 2 +- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index abdc3bab3..d334a7f63 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -174,14 +174,14 @@ CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) { "true", "false", "and", "or", "not", - "text", "value", + "string", "value", 0 }; std::string string = Misc::StringUtils::lowerCase (token.mString); for (int i=0; sKeywords[i]; ++i) - if (sKeywords[i]==string) + if (sKeywords[i]==string || (string.size()==1 && sKeywords[i][0]==string[0])) return Token (static_cast (i+Token::Type_Keyword_True)); return token; @@ -211,7 +211,7 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() case '[': ++mIndex; return Token (Token::Type_OpenSquare); case ']': ++mIndex; return Token (Token::Type_CloseSquare); case ',': ++mIndex; return Token (Token::Type_Comma); - case '?': ++mIndex; return Token (Token::Type_OneShot); + case '!': ++mIndex; return Token (Token::Type_OneShot); } if (c=='"' || c=='_' || std::isalpha (c) || c==':') @@ -224,54 +224,58 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() return Token (Token::Type_None); } -boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty) +boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, bool ignoreOneShot) { if (Token token = getNextToken()) { - switch (token.mType) - { - case Token::Type_Keyword_True: + if (token==Token (Token::Type_OneShot)) + token = getNextToken(); - return boost::shared_ptr (new BooleanNode (true)); + if (token) + switch (token.mType) + { + case Token::Type_Keyword_True: - case Token::Type_Keyword_False: + return boost::shared_ptr (new BooleanNode (true)); - return boost::shared_ptr (new BooleanNode (false)); + case Token::Type_Keyword_False: - case Token::Type_Keyword_And: - case Token::Type_Keyword_Or: + return boost::shared_ptr (new BooleanNode (false)); - return parseNAry (token); + case Token::Type_Keyword_And: + case Token::Type_Keyword_Or: - case Token::Type_Keyword_Not: - { - boost::shared_ptr node = parseImp(); + return parseNAry (token); - if (mError) - return boost::shared_ptr(); + case Token::Type_Keyword_Not: + { + boost::shared_ptr node = parseImp(); - return boost::shared_ptr (new NotNode (node)); - } + if (mError) + return boost::shared_ptr(); - case Token::Type_Keyword_Text: + return boost::shared_ptr (new NotNode (node)); + } - return parseText(); + case Token::Type_Keyword_Text: - case Token::Type_Keyword_Value: + return parseText(); - return parseValue(); + case Token::Type_Keyword_Value: - case Token::Type_EOS: + return parseValue(); - if (!allowEmpty) - error(); + case Token::Type_EOS: - return boost::shared_ptr(); + if (!allowEmpty) + error(); - default: + return boost::shared_ptr(); - error(); - } + default: + + error(); + } } return boost::shared_ptr(); @@ -528,7 +532,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) if (!allowPredefined || token==Token (Token::Type_OneShot)) { - boost::shared_ptr node = parseImp (true); + boost::shared_ptr node = parseImp (true, token!=Token (Token::Type_OneShot)); if (mError) return false; diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index fbaf6972e..5700102cf 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -31,7 +31,7 @@ namespace CSMFilter Token checkKeywords (const Token& token); ///< Turn string token into keyword token, if possible. - boost::shared_ptr parseImp (bool allowEmpty = false); + boost::shared_ptr parseImp (bool allowEmpty = false, bool ignoreOneShot = false); ///< Will return a null-pointer, if there is nothing more to parse. boost::shared_ptr parseNAry (const Token& keyword); From 16331bf1ede22a854a4da06c6c6b8cabd1bd90bd Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 13:26:57 -0700 Subject: [PATCH 125/194] Avoid a hack to play the underwater sound properly --- apps/openmw/mwbase/soundmanager.hpp | 4 +++- apps/openmw/mwsound/soundmanagerimp.cpp | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index b75f6753d..4d764597c 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -39,9 +39,11 @@ namespace MWBase Play_Normal = 0, /* tracked, non-looping, multi-instance, environment */ Play_Loop = 1<<0, /* Sound will continually loop until explicitly stopped */ Play_NoEnv = 1<<1, /* Do not apply environment effects (eg, underwater filters) */ - Play_NoTrack = 1<<2 /* (3D only) Play the sound at the given object's position + Play_NoTrack = 1<<2, /* (3D only) Play the sound at the given object's position * but do not keep it updated (the sound will not move with * the object and will not stop when the object is deleted. */ + + Play_LoopNoEnv = Play_Loop | Play_NoEnv }; enum PlayType { Play_TypeSfx = 1<<3, /* Normal SFX sound */ diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 03e2ce623..ab8f9b8ec 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -550,10 +550,8 @@ namespace MWSound { env = Env_Underwater; //play underwater sound - //HACK: this sound is always played underwater, so set volume and pitch higher (it's then lowered) - //Currently not possible to play looping sound with no environment if(!getSoundPlaying(MWWorld::Ptr(), "Underwater")) - playSound("Underwater", 1.11, 1.42 ,Play_TypeSfx, Play_Loop ); + playSound("Underwater", 1.0f, 1.0f, Play_TypeSfx, Play_LoopNoEnv); } else { From 02df8ab84165d510c33876dd0fddc0172034a4c2 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 13:48:20 -0700 Subject: [PATCH 126/194] Store the underwater sound to easily stop it --- apps/openmw/mwsound/soundmanagerimp.cpp | 11 ++++++----- apps/openmw/mwsound/soundmanagerimp.hpp | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index ab8f9b8ec..00a0aa18e 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -103,6 +103,7 @@ namespace MWSound SoundManager::~SoundManager() { + mUnderwaterSound.reset(); mActiveSounds.clear(); mMusic.reset(); mOutput.reset(); @@ -550,13 +551,13 @@ namespace MWSound { env = Env_Underwater; //play underwater sound - if(!getSoundPlaying(MWWorld::Ptr(), "Underwater")) - playSound("Underwater", 1.0f, 1.0f, Play_TypeSfx, Play_LoopNoEnv); + if(!(mUnderwaterSound && mUnderwaterSound->isPlaying())) + mUnderwaterSound = playSound("Underwater", 1.0f, 1.0f, Play_TypeSfx, Play_LoopNoEnv); } - else + else if(mUnderwaterSound) { - //no need to check if it's playing, stop sound does nothing in that case - stopSound("Underwater"); + mUnderwaterSound->stop(); + mUnderwaterSound.reset(); } mOutput->updateListener( diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 2450ba5c3..f62e62d50 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -44,6 +44,8 @@ namespace MWSound typedef std::map SoundMap; SoundMap mActiveSounds; + MWBase::SoundPtr mUnderwaterSound; + Ogre::Vector3 mListenerPos; Ogre::Vector3 mListenerDir; Ogre::Vector3 mListenerUp; From f216b25be89315b7179912b9e42621cb878e710b Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 16:04:19 -0700 Subject: [PATCH 127/194] Slightly randomize time between environment sounds We should use the "Minimum Time Between Environmental Sounds" and "Maximum Time Between Environmental Sounds" INI/fallback settings, but we don't have them. --- apps/openmw/mwsound/soundmanagerimp.cpp | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 00a0aa18e..372be8393 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -4,11 +4,10 @@ #include #include -#include "../mwworld/esmstore.hpp" - #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "sound_output.hpp" @@ -475,27 +474,32 @@ namespace MWSound void SoundManager::updateRegionSound(float duration) { - MWWorld::Ptr::CellStore *current = MWBase::Environment::get().getWorld()->getPlayer().getPlayer().getCell(); + static float sTimeToNextEnvSound = 0.0f; static int total = 0; static std::string regionName = ""; - static float timePassed = 0.0; + static float sTimePassed = 0.0; + MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Ptr player = world->getPlayer().getPlayer(); + const ESM::Cell *cell = player.getCell()->mCell; - //If the region has changed - timePassed += duration; - if(!current->mCell->isExterior() || timePassed < 10) + sTimePassed += duration; + if(!cell->isExterior() || sTimePassed < sTimeToNextEnvSound) return; - timePassed = 0; - if(regionName != current->mCell->mRegion) + float a = std::rand() / (double)RAND_MAX; + // NOTE: We should use the "Minimum Time Between Environmental Sounds" and + // "Maximum Time Between Environmental Sounds" fallback settings here. + sTimeToNextEnvSound = 5.0f*a + 15.0f*(1.0f-a); + sTimePassed = 0; + + if(regionName != cell->mRegion) { - regionName = current->mCell->mRegion; + regionName = cell->mRegion; total = 0; } - const ESM::Region *regn = - MWBase::Environment::get().getWorld()->getStore().get().search(regionName); - - if (regn == NULL) + const ESM::Region *regn = world->getStore().get().search(regionName); + if(regn == NULL) return; std::vector::const_iterator soundIter; From 189541aa72e2ab7596f0c536fdedbdb9dabaeb69 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 17:08:23 -0700 Subject: [PATCH 128/194] Apply drowning damage based on the update duration 1 damage every 0.33 seconds is 3 damage a second. Applying it this way avoid having to track another stat. --- apps/openmw/mwmechanics/actors.cpp | 44 +++++++++++-------- .../mwmechanics/mechanicsmanagerimp.cpp | 18 +------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index b8d0a8745..8c310ff22 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -11,10 +11,12 @@ #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" @@ -162,33 +164,37 @@ namespace MWMechanics void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration) { - NpcStats &stats = MWWorld::Class::get(ptr).getNpcStats(ptr); - if(MWBase::Environment::get().getWorld()->isSubmerged(ptr) && + MWBase::World *world = MWBase::Environment::get().getWorld(); + NpcStats &stats = ptr.getClass().getNpcStats(ptr); + if(world->isSubmerged(ptr) && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).mMagnitude == 0) { + float timeLeft = 0.0f; if(stats.getFatigue().getCurrent() == 0) stats.setTimeToStartDrowning(0); - - float timeLeft = stats.getTimeToStartDrowning()-duration; - if(timeLeft < 0.0f) - timeLeft = 0.0f; - - stats.setTimeToStartDrowning(timeLeft); + else + { + timeLeft = stats.getTimeToStartDrowning() - duration; + if(timeLeft < 0.0f) + timeLeft = 0.0f; + stats.setTimeToStartDrowning(timeLeft); + } if(timeLeft == 0.0f) - stats.setLastDrowningHitTime(stats.getLastDrowningHitTime()+duration); + { + // If drowning, apply 3 points of damage per second + ptr.getClass().setActorHealth(ptr, stats.getHealth().getCurrent() - 3.0f*duration); + + // Play a drowning sound as necessary for the player + if(ptr == world->getPlayer().getPlayer()) + { + MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); + if(!sndmgr->getSoundPlaying(MWWorld::Ptr(), "drown")) + sndmgr->playSound("drown", 1.0f, 1.0f); + } + } } else - { stats.setTimeToStartDrowning(20); - stats.setLastDrowningHitTime(0); - } - //if npc is drowning and it's time to hit, then hit - while(stats.getTimeToStartDrowning() == 0.0f && stats.getLastDrowningHitTime() > 0.33f) - { - stats.setLastDrowningHitTime(stats.getLastDrowningHitTime()-0.33f); - //fixme: replace it with something different once screen hit effects are implemented (blood on screen) - MWWorld::Class::get(ptr).setActorHealth(ptr, stats.getHealth().getCurrent()-1.0f); - } } Actors::Actors() : mDuration (0) {} diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c559e4445..853b2027e 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -8,7 +8,6 @@ #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" @@ -288,23 +287,10 @@ namespace MWMechanics MWBase::Environment::get().getWindowManager()->setValue ("level", stats.getLevel()); } - //update drowning sound - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWBase::SoundManager * sndmgr = MWBase::Environment::get().getSoundManager(); - MWWorld::Ptr playerPtr = world->getPlayer().getPlayer(); - NpcStats& playerStats = MWWorld::Class::get(playerPtr).getNpcStats(playerPtr); - if(!sndmgr->getSoundPlaying(MWWorld::Ptr(), "drown") && playerStats.getTimeToStartDrowning()==0.0) - { - sndmgr->playSound("drown",1.0,1.0,MWBase::SoundManager::Play_TypeSfx,MWBase::SoundManager::Play_Loop); - } - if(playerStats.getTimeToStartDrowning()>0.0) - { - //no need to check if it's playing, stop sound does nothing in that case - sndmgr->stopSound("drown"); - } - if (mUpdatePlayer) { + MWBase::World *world = MWBase::Environment::get().getWorld(); + // basic player profile; should not change anymore after the creation phase is finished. MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); From 39af9a13fa8d391e8a83ad35c5b8473aa6df3f0f Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 17:13:49 -0700 Subject: [PATCH 129/194] Remove some unused functions --- apps/openmw/mwmechanics/npcstats.cpp | 12 +----------- apps/openmw/mwmechanics/npcstats.hpp | 7 +------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 6a5e5a98f..0b3698289 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -422,7 +422,7 @@ void MWMechanics::NpcStats::modifyProfit(int diff) mProfit += diff; } -float MWMechanics::NpcStats::getTimeToStartDrowning() +float MWMechanics::NpcStats::getTimeToStartDrowning() const { return mTimeToStartDrowning; } @@ -431,13 +431,3 @@ void MWMechanics::NpcStats::setTimeToStartDrowning(float time) assert(time>=0 && time<=20); mTimeToStartDrowning=time; } - -float MWMechanics::NpcStats::getLastDrowningHitTime() -{ - return mLastDrowningHit; -} - -void MWMechanics::NpcStats::setLastDrowningHitTime(float time) -{ - mLastDrowningHit=time; -} diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index d3fe61829..6b7efa5b7 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -147,15 +147,10 @@ namespace MWMechanics int getWerewolfKills() const; - float getTimeToStartDrowning(); + float getTimeToStartDrowning() const; /// Sets time left for the creature to drown if it stays underwater. /// @param time value from [0,20] void setTimeToStartDrowning(float time); - - float getLastDrowningHitTime(); - /// Sets time since last hit caused by drowning. - /// @param time value from [0,0.33] - void setLastDrowningHitTime(float time); }; } From 33c173a23ab4f1a1869dfe396a40827826fe5244 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 17:22:07 -0700 Subject: [PATCH 130/194] Update the watched Ptr when changing it --- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 853b2027e..238aabbaa 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -188,6 +188,9 @@ namespace MWMechanics void MechanicsManager::updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { + if(old == mWatched) + mWatched = ptr; + if(MWWorld::Class::get(ptr).isActor()) mActors.updateActor(old, ptr); else From 281fdbd81bcad454d04552f9dc371303a5925d5a Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 17:56:47 -0700 Subject: [PATCH 131/194] Cleanup some redundancy --- .../mwmechanics/mechanicsmanagerimp.cpp | 73 +++++++++---------- .../mwmechanics/mechanicsmanagerimp.hpp | 3 +- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 238aabbaa..bd5b8e9e3 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -215,79 +215,74 @@ namespace MWMechanics void MechanicsManager::update(float duration, bool paused) { - if (!mWatched.isEmpty()) + if(!mWatched.isEmpty()) { - MWMechanics::CreatureStats& stats = - MWWorld::Class::get (mWatched).getCreatureStats (mWatched); - - MWMechanics::NpcStats& npcStats = - MWWorld::Class::get (mWatched).getNpcStats (mWatched); - - static const char *attributeNames[8] = + static const char attributeNames[8][12] = { "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", "AttribVal5", "AttribVal6", "AttribVal7", "AttribVal8" }; - - static const char *dynamicNames[3] = + static const char dynamicNames[3][5] = { "HBar", "MBar", "FBar" }; - for (int i=0; i<8; ++i) + MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); + for(int i = 0;i < ESM::Attribute::Length;++i) { - if (stats.getAttribute(i)!=mWatchedCreature.getAttribute(i)) + if(stats.getAttribute(i) != mWatchedStats.getAttribute(i)) { - mWatchedCreature.setAttribute(i, stats.getAttribute(i)); - - MWBase::Environment::get().getWindowManager()->setValue (attributeNames[i], stats.getAttribute(i)); + mWatchedStats.setAttribute(i, stats.getAttribute(i)); + winMgr->setValue(attributeNames[i], stats.getAttribute(i)); } } - if (stats.getHealth() != mWatchedCreature.getHealth()) { - mWatchedCreature.setHealth(stats.getHealth()); - MWBase::Environment::get().getWindowManager()->setValue(dynamicNames[0], stats.getHealth()); + if(stats.getHealth() != mWatchedStats.getHealth()) + { + mWatchedStats.setHealth(stats.getHealth()); + winMgr->setValue(dynamicNames[0], stats.getHealth()); } - if (stats.getMagicka() != mWatchedCreature.getMagicka()) { - mWatchedCreature.setMagicka(stats.getMagicka()); - MWBase::Environment::get().getWindowManager()->setValue(dynamicNames[1], stats.getMagicka()); + if(stats.getMagicka() != mWatchedStats.getMagicka()) + { + mWatchedStats.setMagicka(stats.getMagicka()); + winMgr->setValue(dynamicNames[1], stats.getMagicka()); } - if (stats.getFatigue() != mWatchedCreature.getFatigue()) { - mWatchedCreature.setFatigue(stats.getFatigue()); - MWBase::Environment::get().getWindowManager()->setValue(dynamicNames[2], stats.getFatigue()); + if(stats.getFatigue() != mWatchedStats.getFatigue()) + { + mWatchedStats.setFatigue(stats.getFatigue()); + winMgr->setValue(dynamicNames[2], stats.getFatigue()); } - if(npcStats.getTimeToStartDrowning() != mWatchedNpc.getTimeToStartDrowning()) + if(stats.getTimeToStartDrowning() != mWatchedStats.getTimeToStartDrowning()) { - mWatchedNpc.setTimeToStartDrowning(npcStats.getTimeToStartDrowning()); - if(npcStats.getTimeToStartDrowning()>=20.0) - { - MWBase::Environment::get().getWindowManager()->setDrowningBarVisibility(false); - } + mWatchedStats.setTimeToStartDrowning(stats.getTimeToStartDrowning()); + if(stats.getTimeToStartDrowning() >= 20.0f) + winMgr->setDrowningBarVisibility(false); else { - MWBase::Environment::get().getWindowManager()->setDrowningBarVisibility(true); - MWBase::Environment::get().getWindowManager()->setDrowningTimeLeft(npcStats.getTimeToStartDrowning()); + winMgr->setDrowningBarVisibility(true); + winMgr->setDrowningTimeLeft(stats.getTimeToStartDrowning()); } } bool update = false; //Loop over ESM::Skill::SkillEnum - for(int i = 0; i < 27; ++i) + for(int i = 0; i < ESM::Skill::Length; ++i) { - if(npcStats.getSkill (i) != mWatchedNpc.getSkill (i)) + if(stats.getSkill(i) != mWatchedStats.getSkill(i)) { update = true; - mWatchedNpc.getSkill (i) = npcStats.getSkill (i); - MWBase::Environment::get().getWindowManager()->setValue((ESM::Skill::SkillEnum)i, npcStats.getSkill (i)); + mWatchedStats.getSkill(i) = stats.getSkill(i); + winMgr->setValue((ESM::Skill::SkillEnum)i, stats.getSkill(i)); } } - if (update) - MWBase::Environment::get().getWindowManager()->updateSkillArea(); + if(update) + winMgr->updateSkillArea(); - MWBase::Environment::get().getWindowManager()->setValue ("level", stats.getLevel()); + winMgr->setValue("level", stats.getLevel()); } if (mUpdatePlayer) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 95f760d11..ad07562c7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -25,8 +25,7 @@ namespace MWMechanics class MechanicsManager : public MWBase::MechanicsManager { MWWorld::Ptr mWatched; - CreatureStats mWatchedCreature; - NpcStats mWatchedNpc; + NpcStats mWatchedStats; bool mUpdatePlayer; bool mClassSelected; bool mRaceSelected; From 305b5fec0fb26b247daeced91c536c966ff00e53 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 21:40:31 -0700 Subject: [PATCH 132/194] Avoid needlessly copying the MagicEffects --- apps/openmw/mwmechanics/actors.cpp | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 8c310ff22..bfe476adb 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -135,30 +135,27 @@ namespace MWMechanics void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr) { - CreatureStats& creatureStats = MWWorld::Class::get (ptr).getCreatureStats (ptr); + CreatureStats &creatureStats = MWWorld::Class::get(ptr).getCreatureStats(ptr); + const MagicEffects &effects = creatureStats.getMagicEffects(); // attributes - for (int i=0; i<8; ++i) + for(int i = 0;i < ESM::Attribute::Length;++i) { - int modifier = - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyAttribute, i)).mMagnitude; - - modifier -= creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::DrainAttribute, i)).mMagnitude; + Stat stat = creatureStats.getAttribute(i); + stat.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).mMagnitude - + effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).mMagnitude); - creatureStats.getAttribute(i).setModifier (modifier); + creatureStats.setAttribute(i, stat); } // dynamic stats - MagicEffects effects = creatureStats.getMagicEffects(); - - for (int i=0; i<3; ++i) + for(int i = 0;i < 3;++i) { - DynamicStat stat = creatureStats.getDynamic (i); - - stat.setModifier ( - effects.get (EffectKey(80+i)).mMagnitude - effects.get (EffectKey(18+i)).mMagnitude); + DynamicStat stat = creatureStats.getDynamic(i); + stat.setModifier(effects.get(EffectKey(80+i)).mMagnitude - + effects.get(EffectKey(18+i)).mMagnitude); - creatureStats.setDynamic (i, stat); + creatureStats.setDynamic(i, stat); } } From 92082dae66b73cfad33baed01f158d4fffd767bc Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Tue, 27 Aug 2013 22:44:52 -0700 Subject: [PATCH 133/194] Modify the current magicka and fatigue when the base changes --- apps/openmw/mwmechanics/actors.cpp | 20 +++++++++++--------- apps/openmw/mwmechanics/stat.hpp | 12 ++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index bfe476adb..566d4bc50 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -70,22 +70,24 @@ namespace MWMechanics { CreatureStats& creatureStats = MWWorld::Class::get (ptr).getCreatureStats (ptr); - int strength = creatureStats.getAttribute(0).getBase(); - int intelligence = creatureStats.getAttribute(1).getBase(); - int willpower = creatureStats.getAttribute(2).getBase(); - int agility = creatureStats.getAttribute(3).getBase(); - int endurance = creatureStats.getAttribute(5).getBase(); + int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); + int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getBase(); + int willpower = creatureStats.getAttribute(ESM::Attribute::Willpower).getBase(); + int agility = creatureStats.getAttribute(ESM::Attribute::Agility).getBase(); + int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); double magickaFactor = creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).mMagnitude * 0.1 + 0.5; DynamicStat magicka = creatureStats.getMagicka(); - magicka.setBase (static_cast (intelligence + magickaFactor * intelligence)); - creatureStats.setMagicka (magicka); + float diff = (static_cast(intelligence + magickaFactor*intelligence)) - magicka.getBase(); + magicka.modify(diff); + creatureStats.setMagicka(magicka); DynamicStat fatigue = creatureStats.getFatigue(); - fatigue.setBase (strength+willpower+agility+endurance); - creatureStats.setFatigue (fatigue); + diff = (strength+willpower+agility+endurance) - fatigue.getBase(); + fatigue.modify(diff); + creatureStats.setFatigue(fatigue); } void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration) diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index 606671389..65d47c9c0 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -42,6 +42,18 @@ namespace MWMechanics mBase = mModified = value; } + void modify(const T& diff) + { + mBase += diff; + if(mBase >= static_cast(0)) + mModified += diff; + else + { + mModified += diff - mBase; + mBase = static_cast(0); + } + } + /// Set base and adjust modified accordingly. void setBase (const T& value) { From 5918b846669f845a9aa9a3b2889b4fcfb872931b Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 28 Aug 2013 17:04:38 +0200 Subject: [PATCH 134/194] Don't crash on resize events during load --- apps/openmw/mwgui/windowmanagerimp.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index ad1d9a60b..bf8b664da 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -929,6 +929,10 @@ namespace MWGui void WindowManager::windowResized(int x, int y) { + mGuiManager->windowResized(); + mLoadingScreen->onResChange (x,y); + if (!mHud) + return; // UI not initialized yet mHud->onResChange(x, y); mConsole->onResChange(x, y); mMenu->onResChange(x, y); @@ -938,10 +942,8 @@ namespace MWGui mBookWindow->center(); mQuickKeysMenu->center(); mSpellBuyingWindow->center(); - mLoadingScreen->onResChange (x,y); mDragAndDrop->mDragAndDropWidget->setSize(MyGUI::IntSize(x, y)); mInputBlocker->setSize(MyGUI::IntSize(x,y)); - mGuiManager->windowResized(); } void WindowManager::pushGuiMode(GuiMode mode) From 84d259ab8e354148bb2e040a820ea2dc8757b91a Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 28 Aug 2013 10:50:29 -0700 Subject: [PATCH 135/194] Avoid reconstructing strings for updating the dynamic stats Attributes still do this, but they change infrequently enough that it doesn't matter. --- .../mwmechanics/mechanicsmanagerimp.cpp | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index bd5b8e9e3..8c13db790 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -217,41 +217,37 @@ namespace MWMechanics { if(!mWatched.isEmpty()) { - static const char attributeNames[8][12] = - { - "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", "AttribVal5", - "AttribVal6", "AttribVal7", "AttribVal8" - }; - static const char dynamicNames[3][5] = - { - "HBar", "MBar", "FBar" - }; - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); for(int i = 0;i < ESM::Attribute::Length;++i) { if(stats.getAttribute(i) != mWatchedStats.getAttribute(i)) { + std::stringstream attrname; + attrname << "AttribVal"<<(i+1); + mWatchedStats.setAttribute(i, stats.getAttribute(i)); - winMgr->setValue(attributeNames[i], stats.getAttribute(i)); + winMgr->setValue(attrname.str(), stats.getAttribute(i)); } } if(stats.getHealth() != mWatchedStats.getHealth()) { + static const std::string hbar("HBar"); mWatchedStats.setHealth(stats.getHealth()); - winMgr->setValue(dynamicNames[0], stats.getHealth()); + winMgr->setValue(hbar, stats.getHealth()); } if(stats.getMagicka() != mWatchedStats.getMagicka()) { + static const std::string mbar("MBar"); mWatchedStats.setMagicka(stats.getMagicka()); - winMgr->setValue(dynamicNames[1], stats.getMagicka()); + winMgr->setValue(mbar, stats.getMagicka()); } if(stats.getFatigue() != mWatchedStats.getFatigue()) { + static const std::string fbar("FBar"); mWatchedStats.setFatigue(stats.getFatigue()); - winMgr->setValue(dynamicNames[2], stats.getFatigue()); + winMgr->setValue(fbar, stats.getFatigue()); } if(stats.getTimeToStartDrowning() != mWatchedStats.getTimeToStartDrowning()) From e589d0ec91580010d4ce70c310c839b5ebaf4d13 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Wed, 28 Aug 2013 11:36:22 -0700 Subject: [PATCH 136/194] Werewolves can't activate activators --- apps/openmw/mwclass/activator.cpp | 27 +++++++++++++++++++++++++-- apps/openmw/mwclass/activator.hpp | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 3a60d9c39..583cb08d3 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -6,19 +6,26 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld//cellstore.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/physicssystem.hpp" +#include "../mwworld/action.hpp" +#include "../mwworld/failedaction.hpp" +#include "../mwworld/nullaction.hpp" #include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" +#include "../mwmechanics/npcstats.hpp" + + namespace MWClass { - void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); if (!model.empty()) { @@ -94,7 +101,23 @@ namespace MWClass return info; } - + + boost::shared_ptr Activator::activate(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const + { + if(get(actor).isNpc() && get(actor).getNpcStats(actor).isWerewolf()) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Sound *sound = store.get().searchRandom("WolfActivator"); + + boost::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + if(sound) action->setSound(sound->mId); + + return action; + } + return boost::shared_ptr(new MWWorld::NullAction); + } + + MWWorld::Ptr Activator::copyToCellImpl(const MWWorld::Ptr &ptr, MWWorld::CellStore &cell) const { diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 4165fbc08..1e772ef4f 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -31,6 +31,9 @@ namespace MWClass virtual std::string getScript (const MWWorld::Ptr& ptr) const; ///< Return name of the script attached to ptr + virtual boost::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const; + ///< Generate action for activation + static void registerSelf(); virtual std::string getModel(const MWWorld::Ptr &ptr) const; From 9f0b34eae04126fe9ec6cb61fc8abc23980f32ef Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 29 Aug 2013 13:27:54 +0200 Subject: [PATCH 137/194] fixed reference loading code (editor) --- apps/opencs/model/world/cell.cpp | 2 +- apps/opencs/model/world/ref.cpp | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/opencs/model/world/cell.cpp b/apps/opencs/model/world/cell.cpp index fc367d1cc..cd58fca1e 100644 --- a/apps/opencs/model/world/cell.cpp +++ b/apps/opencs/model/world/cell.cpp @@ -7,7 +7,7 @@ void CSMWorld::Cell::load (ESM::ESMReader &esm) { mName = mId; - ESM::Cell::load (esm, true); /// \todo set this to false, once the bug in ESM::Cell::load is fixed + ESM::Cell::load (esm, false); if (!(mData.mFlags & Interior)) { diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index e5e948928..af363bafb 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -8,8 +8,6 @@ void CSMWorld::CellRef::load (ESM::ESMReader &esm, Cell& cell, const std::string mId = id; mCellId = cell.mId; - cell.getNextRef (esm, *this); - if (!mDeleted) cell.addRef (mId); } \ No newline at end of file From c8e31725dcd56c6d60c1946c0b554e6140a42398 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 29 Aug 2013 15:15:40 +0200 Subject: [PATCH 138/194] Fix weather sounds persisting on a new game --- apps/openmw/mwworld/weather.cpp | 5 +++++ apps/openmw/mwworld/weather.hpp | 1 + 2 files changed, 6 insertions(+) diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 587b7847b..6f5dbe23f 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -167,6 +167,11 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa setFallbackWeather(blizzard,"blizzard"); } +WeatherManager::~WeatherManager() +{ + stopSounds(true); +} + void WeatherManager::setWeather(const String& weather, bool instant) { if (weather == mCurrentWeather && mNextWeather == "") diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index a2a07cec7..80cbe0418 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -120,6 +120,7 @@ namespace MWWorld { public: WeatherManager(MWRender::RenderingManager*,MWWorld::Fallback* fallback); + ~WeatherManager(); /** * Change the weather in the specified region From f4e9199f784b434f9833e6e1d0ed291a227f2bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20S=C3=B6derberg?= Date: Thu, 29 Aug 2013 16:21:11 +0200 Subject: [PATCH 139/194] Various chargen tweaks --- files/mygui/openmw_chargen_birth.layout | 8 +-- files/mygui/openmw_chargen_class.layout | 10 +++- .../openmw_chargen_class_description.layout | 2 +- .../mygui/openmw_chargen_create_class.layout | 14 ++++-- files/mygui/openmw_chargen_review.layout | 50 +++++++++---------- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/files/mygui/openmw_chargen_birth.layout b/files/mygui/openmw_chargen_birth.layout index e8d959ba2..b368a6407 100644 --- a/files/mygui/openmw_chargen_birth.layout +++ b/files/mygui/openmw_chargen_birth.layout @@ -1,11 +1,12 @@ - + + - + @@ -13,7 +14,7 @@ - + @@ -24,5 +25,6 @@ + diff --git a/files/mygui/openmw_chargen_class.layout b/files/mygui/openmw_chargen_class.layout index aac97870c..3c0348b66 100644 --- a/files/mygui/openmw_chargen_class.layout +++ b/files/mygui/openmw_chargen_class.layout @@ -1,7 +1,8 @@ - + + @@ -20,6 +21,7 @@ + @@ -32,6 +34,7 @@ + @@ -40,6 +43,7 @@ + @@ -51,6 +55,7 @@ + @@ -60,7 +65,7 @@ - + @@ -71,5 +76,6 @@ + diff --git a/files/mygui/openmw_chargen_class_description.layout b/files/mygui/openmw_chargen_class_description.layout index e003f22c1..eaf754697 100644 --- a/files/mygui/openmw_chargen_class_description.layout +++ b/files/mygui/openmw_chargen_class_description.layout @@ -17,6 +17,6 @@ - + diff --git a/files/mygui/openmw_chargen_create_class.layout b/files/mygui/openmw_chargen_create_class.layout index e9c4146a0..92382640b 100644 --- a/files/mygui/openmw_chargen_create_class.layout +++ b/files/mygui/openmw_chargen_create_class.layout @@ -1,13 +1,15 @@ - + - + + + @@ -19,6 +21,7 @@ + @@ -31,6 +34,7 @@ + @@ -39,6 +43,7 @@ + @@ -50,6 +55,7 @@ + @@ -59,7 +65,7 @@ - + @@ -73,6 +79,6 @@ - + diff --git a/files/mygui/openmw_chargen_review.layout b/files/mygui/openmw_chargen_review.layout index 408d4f471..5d18f4bff 100644 --- a/files/mygui/openmw_chargen_review.layout +++ b/files/mygui/openmw_chargen_review.layout @@ -1,9 +1,9 @@ - + - + @@ -17,27 +17,27 @@ - - - - + + + + - - - + + + - + - + @@ -46,57 +46,57 @@ - - + + - + - + - + - + - + - + - + @@ -105,13 +105,13 @@ - - - + + + - - + + From f12d5b728ad9462a9f99c5905da9ceef8d86a1ed Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 29 Aug 2013 17:14:25 +0200 Subject: [PATCH 140/194] Ogre 1.9 compatibility changes --- apps/openmw/mwrender/videoplayer.cpp | 1 - extern/sdl4ogre/cursormanager.hpp | 5 +---- libs/openengine/bullet/BulletShapeLoader.cpp | 14 +++++++++++++ libs/openengine/bullet/BulletShapeLoader.h | 21 +++++++++++++++++--- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwrender/videoplayer.cpp b/apps/openmw/mwrender/videoplayer.cpp index 8c557ee99..ee2b80f73 100644 --- a/apps/openmw/mwrender/videoplayer.cpp +++ b/apps/openmw/mwrender/videoplayer.cpp @@ -1037,7 +1037,6 @@ public: VideoPlayer::VideoPlayer(Ogre::SceneManager* sceneMgr, Ogre::RenderWindow* window) : mState(NULL) , mSceneMgr(sceneMgr) - , mVideoMaterial(NULL) , mRectangle(NULL) , mNode(NULL) , mAllowSkipping(false) diff --git a/extern/sdl4ogre/cursormanager.hpp b/extern/sdl4ogre/cursormanager.hpp index 1f52eca73..04106fd2f 100644 --- a/extern/sdl4ogre/cursormanager.hpp +++ b/extern/sdl4ogre/cursormanager.hpp @@ -4,10 +4,7 @@ #include #include -namespace Ogre -{ - class TexturePtr; -} +#include namespace SFO { diff --git a/libs/openengine/bullet/BulletShapeLoader.cpp b/libs/openengine/bullet/BulletShapeLoader.cpp index 431d2b91b..5528924a9 100644 --- a/libs/openengine/bullet/BulletShapeLoader.cpp +++ b/libs/openengine/bullet/BulletShapeLoader.cpp @@ -104,6 +104,20 @@ BulletShapeManager::~BulletShapeManager() sThis = 0; } +#if (OGRE_VERSION >= ((1 << 16) | (9 << 8) | 0)) +BulletShapePtr BulletShapeManager::getByName(const Ogre::String& name, const Ogre::String& groupName) +{ + return getResourceByName(name, groupName).staticCast(); +} + +BulletShapePtr BulletShapeManager::create (const Ogre::String& name, const Ogre::String& group, + bool isManual, Ogre::ManualResourceLoader* loader, + const Ogre::NameValuePairList* createParams) +{ + return createResource(name,group,isManual,loader,createParams).staticCast(); +} +#endif + BulletShapePtr BulletShapeManager::load(const Ogre::String &name, const Ogre::String &group) { BulletShapePtr textf = getByName(name); diff --git a/libs/openengine/bullet/BulletShapeLoader.h b/libs/openengine/bullet/BulletShapeLoader.h index a6591010a..98cda859d 100644 --- a/libs/openengine/bullet/BulletShapeLoader.h +++ b/libs/openengine/bullet/BulletShapeLoader.h @@ -48,6 +48,8 @@ public: /** * */ + +#if (OGRE_VERSION < ((1 << 16) | (9 << 8) | 0)) class BulletShapePtr : public Ogre::SharedPtr { public: @@ -91,9 +93,9 @@ public: return *this; } }; - - - +#else +typedef Ogre::SharedPtr BulletShapePtr; +#endif /** *Hold any BulletShape that was created by the ManualBulletShapeLoader. @@ -137,6 +139,19 @@ public: BulletShapeManager(); virtual ~BulletShapeManager(); + +#if (OGRE_VERSION >= ((1 << 16) | (9 << 8) | 0)) + /// Get a resource by name + /// @see ResourceManager::getByName + BulletShapePtr getByName(const Ogre::String& name, const Ogre::String& groupName = Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME); + + /// Create a new shape + /// @see ResourceManager::createResource + BulletShapePtr create (const Ogre::String& name, const Ogre::String& group, + bool isManual = false, Ogre::ManualResourceLoader* loader = 0, + const Ogre::NameValuePairList* createParams = 0); +#endif + virtual BulletShapePtr load(const Ogre::String &name, const Ogre::String &group); static BulletShapeManager &getSingleton(); From f9dbce685a44eff094944dd16c873b13205a29d1 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Thu, 29 Aug 2013 17:07:02 -0700 Subject: [PATCH 141/194] Avoid killing AIWander and AITravel when far away This fixes the problem of certain NPCs not wandering because they happened to spawn near a cell border away from the player, which immediately "completed" the wander package. AIWander can't cause NPCs to cross cell boundaries, so there's no risk of them walking into an unloaded to. AITravel will now simply stop moving, and resume later when the cell is loaded. --- apps/openmw/mwmechanics/aitravel.cpp | 54 ++++++++++++++-------------- apps/openmw/mwmechanics/aiwander.cpp | 43 ++++------------------ 2 files changed, 35 insertions(+), 62 deletions(-) diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 6b43a36f6..d47a49c70 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -33,46 +33,48 @@ namespace MWMechanics bool AiTravel::execute (const MWWorld::Ptr& actor) { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayer().getPlayer(); + MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::Position pos = actor.getRefData().getPosition(); - bool cellChange = actor.getCell()->mCell->mData.mX != cellX || actor.getCell()->mCell->mData.mY != cellY; - const ESM::Pathgrid *pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*actor.getCell()->mCell); + Movement &movement = actor.getClass().getMovementSettings(actor); + const ESM::Cell *cell = actor.getCell()->mCell; - if(actor.getCell()->mCell->mData.mX != player.getCell()->mCell->mData.mX) + MWWorld::Ptr player = world->getPlayer().getPlayer(); + if(cell->mData.mX != player.getCell()->mCell->mData.mX) { - int sideX = sgn(actor.getCell()->mCell->mData.mX - player.getCell()->mCell->mData.mX); - //check if actor is near the border of an inactive cell. If so, disable aitravel. - if(sideX * (pos.pos[0] - actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE) > sideX * (ESM::Land::REAL_SIZE / - 2.0 - 200)) + int sideX = sgn(cell->mData.mX - player.getCell()->mCell->mData.mX); + //check if actor is near the border of an inactive cell. If so, stop walking. + if(sideX * (pos.pos[0] - cell->mData.mX*ESM::Land::REAL_SIZE) > + sideX * (ESM::Land::REAL_SIZE/2.0f - 200.0f)) { - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0; - return true; + movement.mPosition[1] = 0; + return false; } } - if(actor.getCell()->mCell->mData.mY != player.getCell()->mCell->mData.mY) + if(cell->mData.mY != player.getCell()->mCell->mData.mY) { - int sideY = sgn(actor.getCell()->mCell->mData.mY - player.getCell()->mCell->mData.mY); - //check if actor is near the border of an inactive cell. If so, disable aitravel. - if(sideY * (pos.pos[1] - actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE) > sideY * (ESM::Land::REAL_SIZE / - 2.0 - 200)) + int sideY = sgn(cell->mData.mY - player.getCell()->mCell->mData.mY); + //check if actor is near the border of an inactive cell. If so, stop walking. + if(sideY * (pos.pos[1] - cell->mData.mY*ESM::Land::REAL_SIZE) > + sideY * (ESM::Land::REAL_SIZE/2.0f - 200.0f)) { - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0; - return true; + movement.mPosition[1] = 0; + return false; } } + const ESM::Pathgrid *pathgrid = world->getStore().get().search(*cell); + bool cellChange = cell->mData.mX != cellX || cell->mData.mY != cellY; if(!mPathFinder.isPathConstructed() || cellChange) { - cellX = actor.getCell()->mCell->mData.mX; - cellY = actor.getCell()->mCell->mData.mY; + cellX = cell->mData.mX; + cellY = cell->mData.mY; float xCell = 0; float yCell = 0; - if (actor.getCell()->mCell->isExterior()) + if(cell->isExterior()) { - xCell = actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE; - yCell = actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE; + xCell = cell->mData.mX * ESM::Land::REAL_SIZE; + yCell = cell->mData.mY * ESM::Land::REAL_SIZE; } ESM::Pathgrid::Point dest; @@ -90,13 +92,13 @@ namespace MWMechanics if(mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0; + movement.mPosition[1] = 0; return true; } float zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); - MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false); - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1; + world->rotateObject(actor, 0, 0, zAngle, false); + movement.mPosition[1] = 1; return false; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index c4f32f5dc..50191193d 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -65,10 +65,11 @@ namespace MWMechanics bool AiWander::execute (const MWWorld::Ptr& actor) { + MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) { // End package if duration is complete or mid-night hits: - MWWorld::TimeStamp currentTime = MWBase::Environment::get().getWorld()->getTimeStamp(); + MWWorld::TimeStamp currentTime = world->getTimeStamp(); if(currentTime.getHour() >= mStartTime.getHour() + mDuration) { if(!mRepeat) @@ -96,8 +97,7 @@ namespace MWMechanics if(!mStoredAvailableNodes) { mStoredAvailableNodes = true; - mPathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*actor.getCell()->mCell); + mPathgrid = world->getStore().get().search(*actor.getCell()->mCell); mCellX = actor.getCell()->mCell->mData.mX; mCellY = actor.getCell()->mCell->mData.mY; @@ -150,37 +150,8 @@ namespace MWMechanics } } - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayer().getPlayer(); - bool cellChange = actor.getCell()->mCell->mData.mX != mCellX || actor.getCell()->mCell->mData.mY != mCellY; - - if(actor.getCell()->mCell->mData.mX != player.getCell()->mCell->mData.mX) - { - int sideX = sgn(actor.getCell()->mCell->mData.mX - player.getCell()->mCell->mData.mX); - // Check if actor is near the border of an inactive cell. If so, disable AiWander. - // FIXME: This *should* pause the AiWander package instead of terminating it. - if(sideX * (pos.pos[0] - actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE) > sideX * (ESM::Land::REAL_SIZE / - 2.0 - 200)) - { - stopWalking(actor); - return true; - } - } - - if(actor.getCell()->mCell->mData.mY != player.getCell()->mCell->mData.mY) - { - int sideY = sgn(actor.getCell()->mCell->mData.mY - player.getCell()->mCell->mData.mY); - // Check if actor is near the border of an inactive cell. If so, disable AiWander. - // FIXME: This *should* pause the AiWander package instead of terminating it. - if(sideY * (pos.pos[1] - actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE) > sideY * (ESM::Land::REAL_SIZE / - 2.0 - 200)) - { - stopWalking(actor); - return true; - } - } - // Don't try to move if you are in a new cell (ie: positioncell command called) but still play idles. - if(mDistance && (cellChange || (mCellX != actor.getCell()->mCell->mData.mX || mCellY != actor.getCell()->mCell->mData.mY))) + if(mDistance && (mCellX != actor.getCell()->mCell->mData.mX || mCellY != actor.getCell()->mCell->mData.mY)) mDistance = 0; if(mChooseAction) @@ -207,7 +178,7 @@ namespace MWMechanics else { // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: - MWWorld::TimeStamp currentTime = MWBase::Environment::get().getWorld()->getTimeStamp(); + MWWorld::TimeStamp currentTime = world->getTimeStamp(); mStartTime = currentTime; playIdle(actor, mPlayedIdle); mChooseAction = false; @@ -264,7 +235,7 @@ namespace MWMechanics if(mWalking) { float zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); - MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle,false); + world->rotateObject(actor, 0, 0, zAngle, false); MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1; // Unclog path nodes by allowing the NPC to be a small distance away from the center. This way two NPCs can be @@ -275,7 +246,7 @@ namespace MWMechanics actorPos[1] = actorPos[1] - mYCell; float distance = actorPos.squaredDistance(destNodePos); - if(distance < 1200 || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) + if(distance < 32*32 || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { stopWalking(actor); mMoveNow = false; From 9499ac4fd59a68fba884b8f3b9e06ead18c19ba9 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Thu, 29 Aug 2013 17:32:20 -0700 Subject: [PATCH 142/194] Increase the distance for reaching a path node --- apps/openmw/mwmechanics/aiwander.cpp | 10 +--------- apps/openmw/mwmechanics/pathfinding.cpp | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 50191193d..96a41883b 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -238,15 +238,7 @@ namespace MWMechanics world->rotateObject(actor, 0, 0, zAngle, false); MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1; - // Unclog path nodes by allowing the NPC to be a small distance away from the center. This way two NPCs can be - // at the same path node at the same time and both will complete instead of endlessly walking into eachother: - Ogre::Vector3 destNodePos(mCurrentNode.mX, mCurrentNode.mY, mCurrentNode.mZ); - Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos); - actorPos[0] = actorPos[0] - mXCell; - actorPos[1] = actorPos[1] - mYCell; - float distance = actorPos.squaredDistance(destNodePos); - - if(distance < 32*32 || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) + if(mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { stopWalking(actor); mMoveNow = false; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index da9d52e44..7e0e1550b 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -205,7 +205,7 @@ namespace MWMechanics return true; ESM::Pathgrid::Point nextPoint = *mPath.begin(); - if(distanceZCorrected(nextPoint, x, y, z) < 40) + if(distanceZCorrected(nextPoint, x, y, z) < 64) { mPath.pop_front(); if(mPath.empty()) From 1026e89810ca2c70f85063243d040ac4042f3f7b Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Thu, 29 Aug 2013 17:40:21 -0700 Subject: [PATCH 143/194] Don't assume TexturePtr is a class --- extern/sdl4ogre/cursormanager.hpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/extern/sdl4ogre/cursormanager.hpp b/extern/sdl4ogre/cursormanager.hpp index 1f52eca73..06de094f7 100644 --- a/extern/sdl4ogre/cursormanager.hpp +++ b/extern/sdl4ogre/cursormanager.hpp @@ -4,10 +4,7 @@ #include #include -namespace Ogre -{ - class TexturePtr; -} +#include namespace SFO { From 8f23b330d3e48d904bee3bb26378b32b9ee3b77b Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Thu, 29 Aug 2013 17:40:52 -0700 Subject: [PATCH 144/194] Remove unnecessary initialization --- apps/openmw/mwrender/videoplayer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwrender/videoplayer.cpp b/apps/openmw/mwrender/videoplayer.cpp index 8c557ee99..ee2b80f73 100644 --- a/apps/openmw/mwrender/videoplayer.cpp +++ b/apps/openmw/mwrender/videoplayer.cpp @@ -1037,7 +1037,6 @@ public: VideoPlayer::VideoPlayer(Ogre::SceneManager* sceneMgr, Ogre::RenderWindow* window) : mState(NULL) , mSceneMgr(sceneMgr) - , mVideoMaterial(NULL) , mRectangle(NULL) , mNode(NULL) , mAllowSkipping(false) From 82a09a988bcd0b0cd2202b7bc2bfc346234f2ea8 Mon Sep 17 00:00:00 2001 From: Chris Robinson Date: Thu, 29 Aug 2013 19:17:27 -0700 Subject: [PATCH 145/194] Minor pathfinding cleanup --- apps/openmw/mwmechanics/pathfinding.cpp | 22 ++++++---------------- apps/openmw/mwmechanics/pathfinding.hpp | 13 ++++++++----- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 7e0e1550b..8ef0edab8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -147,13 +147,13 @@ namespace MWMechanics mIsPathConstructed = false; } - void PathFinder::buildPath(ESM::Pathgrid::Point startPoint, ESM::Pathgrid::Point endPoint, - const ESM::Pathgrid* pathGrid, float xCell, float yCell, bool allowShortcuts) + void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, + const ESM::Pathgrid *pathGrid, float xCell, float yCell, bool allowShortcuts) { if(allowShortcuts) { - if(MWBase::Environment::get().getWorld()->castRay(startPoint.mX, startPoint.mY, startPoint.mZ, endPoint.mX, endPoint.mY, - endPoint.mZ)) + if(MWBase::Environment::get().getWorld()->castRay(startPoint.mX, startPoint.mY, startPoint.mZ, + endPoint.mX, endPoint.mY, endPoint.mZ)) allowShortcuts = false; } @@ -184,14 +184,14 @@ namespace MWMechanics mIsPathConstructed = false; } - float PathFinder::getZAngleToNext(float x, float y) + float PathFinder::getZAngleToNext(float x, float y) const { // This should never happen (programmers should have an if statement checking mIsPathConstructed that prevents this call // if otherwise). if(mPath.empty()) return 0; - ESM::Pathgrid::Point nextPoint = *mPath.begin(); + const ESM::Pathgrid::Point &nextPoint = *mPath.begin(); float directionX = nextPoint.mX - x; float directionY = nextPoint.mY - y; float directionResult = sqrt(directionX * directionX + directionY * directionY); @@ -217,15 +217,5 @@ namespace MWMechanics return false; } - - std::list PathFinder::getPath() - { - return mPath; - } - - bool PathFinder::isPathConstructed() - { - return mIsPathConstructed; - } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 1727c650f..35e0fa908 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -12,15 +12,18 @@ namespace MWMechanics PathFinder(); void clearPath(); - void buildPath(ESM::Pathgrid::Point startPoint, ESM::Pathgrid::Point endPoint, - const ESM::Pathgrid* pathGrid, float xCell = 0, float yCell = 0, bool allowShortcuts = 1); + void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, + const ESM::Pathgrid* pathGrid, float xCell = 0, float yCell = 0, + bool allowShortcuts = true); bool checkPathCompleted(float x, float y, float z); ///< \Returns true if the last point of the path has been reached. - float getZAngleToNext(float x, float y); + float getZAngleToNext(float x, float y) const; - std::list getPath(); - bool isPathConstructed(); + bool isPathConstructed() const + { + return mIsPathConstructed; + } private: std::list mPath; From da0b90ee45ae7655c16ee152240afb86a7e6ea7a Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Fri, 30 Aug 2013 21:51:23 +0200 Subject: [PATCH 146/194] Converted tests from components/misc into google unittests. Signed-off-by: Lukasz Gromanowski --- apps/openmw_test_suite/CMakeLists.txt | 4 +- .../components/misc/test_slicearray.cpp | 33 ++++++++ .../components/misc/test_stringops.cpp | 76 ++++++++++++++++++- apps/openmw_test_suite/openmw_test_suite.cpp | 4 +- 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 apps/openmw_test_suite/components/misc/test_slicearray.cpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index cdf8bc74b..9fe7890ac 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -11,14 +11,14 @@ if (GTEST_FOUND AND GMOCK_FOUND) include_directories(${GMOCK_INCLUDE_DIRS}) file(GLOB UNITTEST_SRC_FILES - components/misc/*.cpp + components/misc/test_*.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) - target_link_libraries(openmw_test_suite ${GMOCK_BOTH_LIBRARIES} ${GTEST_BOTH_LIBRARIES}) + target_link_libraries(openmw_test_suite ${GMOCK_BOTH_LIBRARIES} ${GTEST_BOTH_LIBRARIES} components) # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) diff --git a/apps/openmw_test_suite/components/misc/test_slicearray.cpp b/apps/openmw_test_suite/components/misc/test_slicearray.cpp new file mode 100644 index 000000000..ab63e56c4 --- /dev/null +++ b/apps/openmw_test_suite/components/misc/test_slicearray.cpp @@ -0,0 +1,33 @@ +#include +#include "components/misc/slice_array.hpp" + +struct SliceArrayTest : public ::testing::Test +{ + protected: + virtual void SetUp() + { + } + + virtual void TearDown() + { + } +}; + +TEST_F(SliceArrayTest, hello_string) +{ + Misc::SString s("hello"); + ASSERT_EQ(sizeof("hello") - 1, s.length); + ASSERT_FALSE(s=="hel"); + ASSERT_FALSE(s=="hell"); + ASSERT_TRUE(s=="hello"); +} + +TEST_F(SliceArrayTest, othello_string_with_offset_2_and_size_4) +{ + Misc::SString s("othello" + 2, 4); + ASSERT_EQ(sizeof("hell") - 1, s.length); + ASSERT_FALSE(s=="hel"); + ASSERT_TRUE(s=="hell"); + ASSERT_FALSE(s=="hello"); +} + diff --git a/apps/openmw_test_suite/components/misc/test_stringops.cpp b/apps/openmw_test_suite/components/misc/test_stringops.cpp index b12584196..3ae033db7 100644 --- a/apps/openmw_test_suite/components/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/components/misc/test_stringops.cpp @@ -1,7 +1,79 @@ #include +#include "components/misc/stringops.hpp" -TEST(simple_test, dummy) +struct StringOpsTest : public ::testing::Test { - EXPECT_EQ(true, true); + protected: + virtual void SetUp() + { + } + + virtual void TearDown() + { + } +}; + +TEST_F(StringOpsTest, begins_matching) +{ + ASSERT_EQ(true, Misc::begins("abc", "a")); + ASSERT_EQ(true, Misc::begins("abc", "ab")); + ASSERT_EQ(true, Misc::begins("abc", "abc")); + ASSERT_EQ(true, Misc::begins("abcd", "abc")); +} + +TEST_F(StringOpsTest, begins_not_matching) +{ + ASSERT_EQ(false, Misc::begins("abc", "b")); + ASSERT_EQ(false, Misc::begins("abc", "bc")); + ASSERT_EQ(false, Misc::begins("abc", "bcd")); + ASSERT_EQ(false, Misc::begins("abc", "abcd")); +} + +TEST_F(StringOpsTest, ibegins_matching) +{ + ASSERT_EQ(true, Misc::ibegins("Abc", "a")); + ASSERT_EQ(true, Misc::ibegins("aBc", "ab")); + ASSERT_EQ(true, Misc::ibegins("abC", "abc")); + ASSERT_EQ(true, Misc::ibegins("abcD", "abc")); +} + +TEST_F(StringOpsTest, ibegins_not_matching) +{ + ASSERT_EQ(false, Misc::ibegins("abc", "b")); + ASSERT_EQ(false, Misc::ibegins("abc", "bc")); + ASSERT_EQ(false, Misc::ibegins("abc", "bcd")); + ASSERT_EQ(false, Misc::ibegins("abc", "abcd")); +} + +TEST_F(StringOpsTest, ends_matching) +{ + ASSERT_EQ(true, Misc::ends("abc", "c")); + ASSERT_EQ(true, Misc::ends("abc", "bc")); + ASSERT_EQ(true, Misc::ends("abc", "abc")); + ASSERT_EQ(true, Misc::ends("abcd", "abcd")); +} + +TEST_F(StringOpsTest, ends_not_matching) +{ + ASSERT_EQ(false, Misc::ends("abc", "b")); + ASSERT_EQ(false, Misc::ends("abc", "ab")); + ASSERT_EQ(false, Misc::ends("abc", "bcd")); + ASSERT_EQ(false, Misc::ends("abc", "abcd")); +} + +TEST_F(StringOpsTest, iends_matching) +{ + ASSERT_EQ(true, Misc::iends("Abc", "c")); + ASSERT_EQ(true, Misc::iends("aBc", "bc")); + ASSERT_EQ(true, Misc::iends("abC", "abc")); + ASSERT_EQ(true, Misc::iends("abcD", "abcd")); +} + +TEST_F(StringOpsTest, iends_not_matching) +{ + ASSERT_EQ(false, Misc::iends("abc", "b")); + ASSERT_EQ(false, Misc::iends("abc", "ab")); + ASSERT_EQ(false, Misc::iends("abc", "bcd")); + ASSERT_EQ(false, Misc::iends("abc", "abcd")); } diff --git a/apps/openmw_test_suite/openmw_test_suite.cpp b/apps/openmw_test_suite/openmw_test_suite.cpp index 0a09b3028..81476325e 100644 --- a/apps/openmw_test_suite/openmw_test_suite.cpp +++ b/apps/openmw_test_suite/openmw_test_suite.cpp @@ -5,8 +5,8 @@ int main(int argc, char** argv) { // The following line causes Google Mock to throw an exception on failure, // which will be interpreted by your testing framework as a test failure. - ::testing::GTEST_FLAG(throw_on_failure) = true; + ::testing::GTEST_FLAG(throw_on_failure) = false; ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} From 515a865daafae550756a36f4787f79c65deb015d Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Fri, 30 Aug 2013 22:44:03 +0200 Subject: [PATCH 147/194] Changed ASSERT_EQ to ASSERT_TRUE/FALSE. Signed-off-by: Lukasz Gromanowski --- .../components/misc/test_stringops.cpp | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/openmw_test_suite/components/misc/test_stringops.cpp b/apps/openmw_test_suite/components/misc/test_stringops.cpp index 3ae033db7..44587c445 100644 --- a/apps/openmw_test_suite/components/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/components/misc/test_stringops.cpp @@ -15,65 +15,65 @@ struct StringOpsTest : public ::testing::Test TEST_F(StringOpsTest, begins_matching) { - ASSERT_EQ(true, Misc::begins("abc", "a")); - ASSERT_EQ(true, Misc::begins("abc", "ab")); - ASSERT_EQ(true, Misc::begins("abc", "abc")); - ASSERT_EQ(true, Misc::begins("abcd", "abc")); + ASSERT_TRUE(Misc::begins("abc", "a")); + ASSERT_TRUE(Misc::begins("abc", "ab")); + ASSERT_TRUE(Misc::begins("abc", "abc")); + ASSERT_TRUE(Misc::begins("abcd", "abc")); } TEST_F(StringOpsTest, begins_not_matching) { - ASSERT_EQ(false, Misc::begins("abc", "b")); - ASSERT_EQ(false, Misc::begins("abc", "bc")); - ASSERT_EQ(false, Misc::begins("abc", "bcd")); - ASSERT_EQ(false, Misc::begins("abc", "abcd")); + ASSERT_FALSE(Misc::begins("abc", "b")); + ASSERT_FALSE(Misc::begins("abc", "bc")); + ASSERT_FALSE(Misc::begins("abc", "bcd")); + ASSERT_FALSE(Misc::begins("abc", "abcd")); } TEST_F(StringOpsTest, ibegins_matching) { - ASSERT_EQ(true, Misc::ibegins("Abc", "a")); - ASSERT_EQ(true, Misc::ibegins("aBc", "ab")); - ASSERT_EQ(true, Misc::ibegins("abC", "abc")); - ASSERT_EQ(true, Misc::ibegins("abcD", "abc")); + ASSERT_TRUE(Misc::ibegins("Abc", "a")); + ASSERT_TRUE(Misc::ibegins("aBc", "ab")); + ASSERT_TRUE(Misc::ibegins("abC", "abc")); + ASSERT_TRUE(Misc::ibegins("abcD", "abc")); } TEST_F(StringOpsTest, ibegins_not_matching) { - ASSERT_EQ(false, Misc::ibegins("abc", "b")); - ASSERT_EQ(false, Misc::ibegins("abc", "bc")); - ASSERT_EQ(false, Misc::ibegins("abc", "bcd")); - ASSERT_EQ(false, Misc::ibegins("abc", "abcd")); + ASSERT_FALSE(Misc::ibegins("abc", "b")); + ASSERT_FALSE(Misc::ibegins("abc", "bc")); + ASSERT_FALSE(Misc::ibegins("abc", "bcd")); + ASSERT_FALSE(Misc::ibegins("abc", "abcd")); } TEST_F(StringOpsTest, ends_matching) { - ASSERT_EQ(true, Misc::ends("abc", "c")); - ASSERT_EQ(true, Misc::ends("abc", "bc")); - ASSERT_EQ(true, Misc::ends("abc", "abc")); - ASSERT_EQ(true, Misc::ends("abcd", "abcd")); + ASSERT_TRUE(Misc::ends("abc", "c")); + ASSERT_TRUE(Misc::ends("abc", "bc")); + ASSERT_TRUE(Misc::ends("abc", "abc")); + ASSERT_TRUE(Misc::ends("abcd", "abcd")); } TEST_F(StringOpsTest, ends_not_matching) { - ASSERT_EQ(false, Misc::ends("abc", "b")); - ASSERT_EQ(false, Misc::ends("abc", "ab")); - ASSERT_EQ(false, Misc::ends("abc", "bcd")); - ASSERT_EQ(false, Misc::ends("abc", "abcd")); + ASSERT_FALSE(Misc::ends("abc", "b")); + ASSERT_FALSE(Misc::ends("abc", "ab")); + ASSERT_FALSE(Misc::ends("abc", "bcd")); + ASSERT_FALSE(Misc::ends("abc", "abcd")); } TEST_F(StringOpsTest, iends_matching) { - ASSERT_EQ(true, Misc::iends("Abc", "c")); - ASSERT_EQ(true, Misc::iends("aBc", "bc")); - ASSERT_EQ(true, Misc::iends("abC", "abc")); - ASSERT_EQ(true, Misc::iends("abcD", "abcd")); + ASSERT_TRUE(Misc::iends("Abc", "c")); + ASSERT_TRUE(Misc::iends("aBc", "bc")); + ASSERT_TRUE(Misc::iends("abC", "abc")); + ASSERT_TRUE(Misc::iends("abcD", "abcd")); } TEST_F(StringOpsTest, iends_not_matching) { - ASSERT_EQ(false, Misc::iends("abc", "b")); - ASSERT_EQ(false, Misc::iends("abc", "ab")); - ASSERT_EQ(false, Misc::iends("abc", "bcd")); - ASSERT_EQ(false, Misc::iends("abc", "abcd")); + ASSERT_FALSE(Misc::iends("abc", "b")); + ASSERT_FALSE(Misc::iends("abc", "ab")); + ASSERT_FALSE(Misc::iends("abc", "bcd")); + ASSERT_FALSE(Misc::iends("abc", "abcd")); } From 303e02cab5930155e9d9ce3caa6893d6fbdd56a5 Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Fri, 30 Aug 2013 22:45:30 +0200 Subject: [PATCH 148/194] Added unittests for FileFinder::find function. Signed-off-by: Lukasz Gromanowski --- apps/openmw_test_suite/CMakeLists.txt | 1 + .../components/file_finder/test_search.cpp | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 apps/openmw_test_suite/components/file_finder/test_search.cpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 9fe7890ac..682010340 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -12,6 +12,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) file(GLOB UNITTEST_SRC_FILES components/misc/test_*.cpp + components/file_finder/test_*.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/components/file_finder/test_search.cpp b/apps/openmw_test_suite/components/file_finder/test_search.cpp new file mode 100644 index 000000000..2ca940e6a --- /dev/null +++ b/apps/openmw_test_suite/components/file_finder/test_search.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "components/file_finder/search.hpp" + +struct SearchTest : public ::testing::Test +{ + protected: + SearchTest() + : mTestDir("./test_dir/") + { + } + + virtual void SetUp() + { + boost::filesystem::create_directory(boost::filesystem::path(mTestDir)); + + std::ofstream ofs(std::string(mTestDir + "test2.txt").c_str(), std::ofstream::out); + ofs << std::endl; + ofs.close(); + } + + virtual void TearDown() + { + boost::filesystem::remove_all(boost::filesystem::path(mTestDir)); + } + + std::string mTestDir; +}; + +TEST_F(SearchTest, file_not_found) +{ + struct Result : public FileFinder::ReturnPath + { + Result(const boost::filesystem::path& expectedPath) + : mExpectedPath(expectedPath) + { + } + + void add(const boost::filesystem::path& p) + { + ASSERT_FALSE(p == mExpectedPath); + } + + private: + boost::filesystem::path mExpectedPath; + + } r(boost::filesystem::path(mTestDir + "test.txt")); + + FileFinder::find(mTestDir, r, false); +} + +TEST_F(SearchTest, file_found) +{ + struct Result : public FileFinder::ReturnPath + { + Result(const boost::filesystem::path& expectedPath) + : mExpectedPath(expectedPath) + { + } + + void add(const boost::filesystem::path& p) + { + ASSERT_TRUE(p == mExpectedPath); + } + + private: + boost::filesystem::path mExpectedPath; + + } r(boost::filesystem::path(mTestDir + "test2.txt")); + + FileFinder::find(mTestDir, r, false); +} From 2baaef7d87eface596ab5e23cc113f26583568e1 Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Fri, 30 Aug 2013 23:23:34 +0200 Subject: [PATCH 149/194] Added unittests for FileFinder::FileFinder class. Signed-off-by: Lukasz Gromanowski --- .../file_finder/test_filefinder.cpp | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 apps/openmw_test_suite/components/file_finder/test_filefinder.cpp diff --git a/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp b/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp new file mode 100644 index 000000000..cf34570d7 --- /dev/null +++ b/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp @@ -0,0 +1,52 @@ +//FileFinderT +#include +#include +#include "components/file_finder/file_finder.hpp" + +struct FileFinderTest : public ::testing::Test +{ + protected: + FileFinderTest() + : mTestDir("./filefinder_test_dir/") + , mTestFile("test.txt") + , mTestFileUppercase("TEST.TXT") + , mTestFileNotFound("foobarbaz.txt") + { + } + + virtual void SetUp() + { + boost::filesystem::create_directory(boost::filesystem::path(mTestDir)); + + std::ofstream ofs(std::string(mTestDir + mTestFile).c_str(), std::ofstream::out); + ofs << std::endl; + ofs.close(); + } + + virtual void TearDown() + { + boost::filesystem::remove_all(boost::filesystem::path(mTestDir)); + } + + std::string mTestDir; + std::string mTestFile; + std::string mTestFileUppercase; + std::string mTestFileNotFound; +}; + +TEST_F(FileFinderTest, FileFinder_has_file) +{ + FileFinder::FileFinder fileFinder(mTestDir); + ASSERT_TRUE(fileFinder.has(mTestFile)); + ASSERT_TRUE(fileFinder.has(mTestFileUppercase)); + ASSERT_TRUE(fileFinder.lookup(mTestFile) == std::string(mTestDir + mTestFile)); + ASSERT_TRUE(fileFinder.lookup(mTestFileUppercase) == std::string(mTestDir + mTestFile)); +} + +TEST_F(FileFinderTest, FileFinder_does_not_have_file) +{ + FileFinder::FileFinder fileFinder(mTestDir); + ASSERT_FALSE(fileFinder.has(mTestFileNotFound)); + ASSERT_TRUE(fileFinder.lookup(mTestFileNotFound).empty()); +} + From 0e83b5065953d70a2e7dd354eb468b33ae317a09 Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Fri, 30 Aug 2013 23:24:33 +0200 Subject: [PATCH 150/194] Changed test dir for SearchTest. Signed-off-by: Lukasz Gromanowski --- apps/openmw_test_suite/components/file_finder/test_search.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw_test_suite/components/file_finder/test_search.cpp b/apps/openmw_test_suite/components/file_finder/test_search.cpp index 2ca940e6a..63745b625 100644 --- a/apps/openmw_test_suite/components/file_finder/test_search.cpp +++ b/apps/openmw_test_suite/components/file_finder/test_search.cpp @@ -8,7 +8,7 @@ struct SearchTest : public ::testing::Test { protected: SearchTest() - : mTestDir("./test_dir/") + : mTestDir("./search_test_dir/") { } From 2c49458b6cfa5164d425248051c9c48ea84819b3 Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Sat, 31 Aug 2013 00:13:42 +0200 Subject: [PATCH 151/194] Added unittests for FileFinder::FileFinderStrict class. Signed-off-by: Lukasz Gromanowski --- .../components/file_finder/test_filefinder.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp b/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp index cf34570d7..2d151988b 100644 --- a/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp +++ b/apps/openmw_test_suite/components/file_finder/test_filefinder.cpp @@ -1,4 +1,3 @@ -//FileFinderT #include #include #include "components/file_finder/file_finder.hpp" @@ -50,3 +49,18 @@ TEST_F(FileFinderTest, FileFinder_does_not_have_file) ASSERT_TRUE(fileFinder.lookup(mTestFileNotFound).empty()); } +TEST_F(FileFinderTest, FileFinderStrict_has_file) +{ + FileFinder::FileFinderStrict fileFinder(mTestDir); + ASSERT_TRUE(fileFinder.has(mTestFile)); + ASSERT_FALSE(fileFinder.has(mTestFileUppercase)); + ASSERT_TRUE(fileFinder.lookup(mTestFile) == std::string(mTestDir + mTestFile)); + ASSERT_FALSE(fileFinder.lookup(mTestFileUppercase) == std::string(mTestDir + mTestFile)); +} + +TEST_F(FileFinderTest, FileFinderStrict_does_not_have_file) +{ + FileFinder::FileFinderStrict fileFinder(mTestDir); + ASSERT_FALSE(fileFinder.has(mTestFileNotFound)); + ASSERT_TRUE(fileFinder.lookup(mTestFileNotFound).empty()); +} From 3ddeb075fbba25ca4aac64bd9282ec4d9da791e0 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 31 Aug 2013 04:36:23 +0200 Subject: [PATCH 152/194] Fix a cell border seam on the map --- apps/openmw/mwrender/localmap.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index c41484452..5f4128978 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -370,8 +370,8 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni MWBase::Environment::get().getWindowManager()->setPlayerDir(playerdirection.x, playerdirection.y); // explore radius (squared) - const float sqrExploreRadius = (mInterior ? 0.01 : 0.09) * sFogOfWarResolution*sFogOfWarResolution; - const float exploreRadius = (mInterior ? 0.1 : 0.3) * sFogOfWarResolution; // explore radius from 0 to sFogOfWarResolution + const float exploreRadius = (mInterior ? 0.1 : 0.3) * (sFogOfWarResolution-1); // explore radius from 0 to sFogOfWarResolution-1 + const float sqrExploreRadius = Math::Sqr(exploreRadius); const float exploreRadiusUV = exploreRadius / sFogOfWarResolution; // explore radius from 0 to 1 (UV space) // change the affected fog of war textures (in a 3x3 grid around the player) @@ -406,11 +406,8 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni { for (int texU = 0; texU> 24); alpha = std::min( alpha, (uint8) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); From f9cfe654f2f15bf9e774f625395f7a0e9ef663d9 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Sun, 1 Sep 2013 23:17:41 +0400 Subject: [PATCH 153/194] Workaround for https://bugreports.qt-project.org/browse/QTBUG-22154 (Qt redefines min OS X version and SDL in turn checks this version and doesn't accept version set by Qt) --- apps/launcher/graphicspage.cpp | 5 +++++ apps/launcher/main.cpp | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 1bbf7f897..9308c1d57 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -3,6 +3,11 @@ #include #include #include + +#ifdef __APPLE__ +// We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 +#define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#endif #include #include diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index dfe2d7413..f67f5edcf 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -3,6 +3,10 @@ #include #include +#ifdef __APPLE__ +// We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 +#define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#endif #include #include "maindialog.hpp" From 4dbacdc6bd0172942617f2470e70b3b3321a62c5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 2 Sep 2013 10:58:58 +0200 Subject: [PATCH 154/194] increased version number --- CMakeLists.txt | 2 +- readme.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d6daf9f0..3f95f2d4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ include (OpenMWMacros) # Version set (OPENMW_VERSION_MAJOR 0) -set (OPENMW_VERSION_MINOR 25) +set (OPENMW_VERSION_MINOR 26) set (OPENMW_VERSION_RELEASE 0) set (OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}") diff --git a/readme.txt b/readme.txt index d00a959a0..ade5da04d 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ OpenMW: A reimplementation of The Elder Scrolls III: Morrowind OpenMW is an attempt at recreating the engine for the popular role-playing game Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. -Version: 0.25.0 +Version: 0.26.0 License: GPL (see GPL3.txt for more information) Website: http://www.openmw.org @@ -71,13 +71,13 @@ Allowed options: --new-game [=arg(=1)] (=0) activate char gen/new game mechanics --fs-strict [=arg(=1)] (=0) strict file system handling (no case folding) --encoding arg (=win1252) Character encoding used in OpenMW game messages: - + win1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages - + win1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages - + win1252 - Western European (Latin) alphabet, used by default - + --fallback arg fallback values CHANGELOG From 649ef6f12010b2bbd24521edab214867e14bddcc Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 2 Sep 2013 11:58:05 +0200 Subject: [PATCH 155/194] moved list of enum literals from ViewManager to Columns (which makes them available to the rest of model now) --- apps/opencs/model/world/columns.cpp | 81 +++++++++++++++++++++ apps/opencs/model/world/columns.hpp | 6 ++ apps/opencs/view/doc/viewmanager.cpp | 97 +++++++------------------ apps/opencs/view/world/enumdelegate.cpp | 12 +++ apps/opencs/view/world/enumdelegate.hpp | 3 + 5 files changed, 128 insertions(+), 71 deletions(-) diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index b20632258..73a58ac82 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -196,4 +196,85 @@ int CSMWorld::Columns::getId (const std::string& name) return sNames[i].mId; return -1; +} + +namespace +{ + static const char *sSpecialisations[] = + { + "Combat", "Magic", "Stealth", 0 + }; + + static const char *sAttributes[] = + { + "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", + "Luck", 0 + }; + + static const char *sSpellTypes[] = + { + "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 0 + }; + + static const char *sApparatusTypes[] = + { + "Mortar & Pestle", "Albemic", "Calcinator", "Retort", 0 + }; + + static const char *sArmorTypes[] = + { + "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", "Left Gauntlet", + "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", 0 + }; + + static const char *sClothingTypes[] = + { + "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", + "Amulet", 0 + }; + + static const char *sCreatureTypes[] = + { + "Creature", "Deadra", "Undead", "Humanoid", 0 + }; + + static const char *sWeaponTypes[] = + { + "Short Blade 1H", "Long Blade 1H", "Long Blade 2H", "Blunt 1H", "Blunt 2H Close", + "Blunt 2H Wide", "Spear 2H", "Axe 1H", "Axe 2H", "Bow", "Crossbow", "Thrown", "Arrow", + "Bolt", 0 + }; + + const char **getEnumNames (CSMWorld::Columns::ColumnId column) + { + switch (column) + { + case CSMWorld::Columns::ColumnId_Specialisation: return sSpecialisations; + case CSMWorld::Columns::ColumnId_Attribute: return sAttributes; + case CSMWorld::Columns::ColumnId_SpellType: return sSpellTypes; + case CSMWorld::Columns::ColumnId_ApparatusType: return sApparatusTypes; + case CSMWorld::Columns::ColumnId_ArmorType: return sArmorTypes; + case CSMWorld::Columns::ColumnId_ClothingType: return sClothingTypes; + case CSMWorld::Columns::ColumnId_CreatureType: return sCreatureTypes; + case CSMWorld::Columns::ColumnId_WeaponType: return sWeaponTypes; + + default: return 0; + } + } +} + +bool CSMWorld::Columns::hasEnums (ColumnId column) +{ + return getEnumNames (column)!=0; +} + +std::vector CSMWorld::Columns::getEnums (ColumnId column) +{ + std::vector enums; + + if (const char **table = getEnumNames (column)) + for (int i=0; table[i]; ++i) + enums.push_back (table[i]); + + return enums; } \ No newline at end of file diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 9a39e1678..69b20583a 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -2,6 +2,7 @@ #define CSM_WOLRD_COLUMNS_H #include +#include namespace CSMWorld { @@ -180,6 +181,11 @@ namespace CSMWorld int getId (const std::string& name); ///< Will return -1 for an invalid name. + + bool hasEnums (ColumnId column); + + std::vector getEnums (ColumnId column); + ///< Returns an empty vector, if \æ column isn't an enum type column. } } diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 6d06e4248..7c7c0f28b 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -8,6 +8,7 @@ #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" +#include "../../model/world/columns.hpp" #include "../world/util.hpp" #include "../world/enumdelegate.hpp" @@ -43,51 +44,6 @@ void CSVDoc::ViewManager::updateIndices() CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) : mDocumentManager (documentManager), mExitOnSaveStateChange(false), mUserWarned(false) { - static const char *sSpecialisations[] = - { - "Combat", "Magic", "Stealth", 0 - }; - - static const char *sAttributes[] = - { - "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", - "Luck", 0 - }; - - static const char *sSpellTypes[] = - { - "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 0 - }; - - static const char *sApparatusTypes[] = - { - "Mortar & Pestle", "Albemic", "Calcinator", "Retort", 0 - }; - - static const char *sArmorTypes[] = - { - "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", "Left Gauntlet", - "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", 0 - }; - - static const char *sClothingTypes[] = - { - "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", - "Amulet", 0 - }; - - static const char *sCreatureTypes[] = - { - "Creature", "Deadra", "Undead", "Humanoid", 0 - }; - - static const char *sWeaponTypes[] = - { - "Short Blade 1H", "Long Blade 1H", "Long Blade 2H", "Blunt 1H", "Blunt 2H Close", - "Blunt 2H Wide", "Spear 2H", "Axe 1H", "Axe 2H", "Bow", "Crossbow", "Thrown", "Arrow", - "Bolt", 0 - }; - mDelegateFactories = new CSVWorld::CommandDelegateFactoryCollection; mDelegateFactories->add (CSMWorld::ColumnBase::Display_GmstVarType, @@ -96,38 +52,37 @@ CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) mDelegateFactories->add (CSMWorld::ColumnBase::Display_GlobalVarType, new CSVWorld::VarTypeDelegateFactory (ESM::VT_Short, ESM::VT_Long, ESM::VT_Float)); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_Specialisation, - new CSVWorld::EnumDelegateFactory (sSpecialisations)); - - mDelegateFactories->add (CSMWorld::ColumnBase::Display_Attribute, - new CSVWorld::EnumDelegateFactory (sAttributes, true)); - - mDelegateFactories->add (CSMWorld::ColumnBase::Display_SpellType, - new CSVWorld::EnumDelegateFactory (sSpellTypes)); - - mDelegateFactories->add (CSMWorld::ColumnBase::Display_ApparatusType, - new CSVWorld::EnumDelegateFactory (sApparatusTypes)); - - mDelegateFactories->add (CSMWorld::ColumnBase::Display_ArmorType, - new CSVWorld::EnumDelegateFactory (sArmorTypes)); - - mDelegateFactories->add (CSMWorld::ColumnBase::Display_ClothingType, - new CSVWorld::EnumDelegateFactory (sClothingTypes)); + mDelegateFactories->add (CSMWorld::ColumnBase::Display_RecordState, + new CSVWorld::RecordStatusDelegateFactory()); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_CreatureType, - new CSVWorld::EnumDelegateFactory (sCreatureTypes)); + mDelegateFactories->add (CSMWorld::ColumnBase::Display_RefRecordType, + new CSVWorld::RefIdTypeDelegateFactory()); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_WeaponType, - new CSVWorld::EnumDelegateFactory (sWeaponTypes)); + struct Mapping + { + CSMWorld::ColumnBase::Display mDisplay; + CSMWorld::Columns::ColumnId mColumnId; + bool mAllowNone; + }; - mDelegateFactories->add (CSMWorld::ColumnBase::Display_RecordState, - new CSVWorld::RecordStatusDelegateFactory() ); + static const Mapping sMapping[] = + { + { CSMWorld::ColumnBase::Display_Specialisation, CSMWorld::Columns::ColumnId_Specialisation, false }, + { CSMWorld::ColumnBase::Display_Attribute, CSMWorld::Columns::ColumnId_Attribute, true }, + { CSMWorld::ColumnBase::Display_SpellType, CSMWorld::Columns::ColumnId_SpellType, false }, + { CSMWorld::ColumnBase::Display_ApparatusType, CSMWorld::Columns::ColumnId_ApparatusType, false }, + { CSMWorld::ColumnBase::Display_ArmorType, CSMWorld::Columns::ColumnId_ArmorType, false }, + { CSMWorld::ColumnBase::Display_ClothingType, CSMWorld::Columns::ColumnId_ClothingType, false }, + { CSMWorld::ColumnBase::Display_CreatureType, CSMWorld::Columns::ColumnId_CreatureType, false }, + { CSMWorld::ColumnBase::Display_WeaponType, CSMWorld::Columns::ColumnId_WeaponType, false } + }; - mDelegateFactories->add (CSMWorld::ColumnBase::Display_RefRecordType, - new CSVWorld::RefIdTypeDelegateFactory() ); + for (std::size_t i=0; iadd (sMapping[i].mDisplay, new CSVWorld::EnumDelegateFactory ( + CSMWorld::Columns::getEnums (sMapping[i].mColumnId), sMapping[i].mAllowNone)); connect (&CSMSettings::UserSettings::instance(), SIGNAL (signalUpdateEditorSetting (const QString &, const QString &)), - this, SLOT (slotUpdateEditorSetting (const QString &, const QString &))); + this, SLOT (slotUpdateEditorSetting (const QString &, const QString &))); } CSVDoc::ViewManager::~ViewManager() diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index dd194abe9..fc9b7ee3b 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -109,6 +109,18 @@ CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const char **names, bool all add (i, names[i]); } +CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const std::vector& names, + bool allowNone) +{ + if (allowNone) + add (-1, ""); + + int size = static_cast (names.size()); + + for (int i=0; i& names, bool allowNone = false); + /// \param allowNone Use value of -1 for "none selected" (empty string) + virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. From 9cb121bd049d2ce381f80a3c0c2165d5093933c2 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 2 Sep 2013 12:23:19 +0200 Subject: [PATCH 156/194] allow specifying enums as text in filters --- apps/opencs/model/filter/textnode.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index 9987c66d2..a826a2d54 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -28,13 +28,30 @@ bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, QVariant data = table.data (index); - if (data.type()!=QVariant::String) + QString string; + + if (data.type()==QVariant::String) + { + string = data.toString(); + } + else if (data.type()==QVariant::Int || data.type()==QVariant::UInt || + CSMWorld::Columns::hasEnums (static_cast (mColumnId))) + { + int value = data.toInt(); + + std::vector enums = + CSMWorld::Columns::getEnums (static_cast (mColumnId)); + + if (value>=0 && value (enums.size())) + string = QString::fromUtf8 (enums[value].c_str()); + } + else return false; /// \todo make pattern syntax configurable QRegExp regExp (QString::fromUtf8 (mText.c_str()), Qt::CaseInsensitive); - return regExp.exactMatch (data.toString()); + return regExp.exactMatch (string); } std::vector CSMFilter::TextNode::getReferencedColumns() const From 51332b86a118704bc73cae43556b83a4591399b1 Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Tue, 3 Sep 2013 01:44:21 -0500 Subject: [PATCH 157/194] [Feature #811] Only 1 Instance of OpenCS is Allowed If another instance of OpenCS is started, then it will terminate. This is done by creating a QLocalServer with a unique ID. If another QLocalServer with the same ID attempts to be opened, then the creation of the QLocalServer will fail and the application will terminate. --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/editor.cpp | 11 +++++++++++ apps/opencs/editor.hpp | 8 ++++++++ apps/opencs/main.cpp | 5 +++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index f7b7daee4..1eb8fd276 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -127,7 +127,7 @@ if(WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) -find_package(Qt4 COMPONENTS QtCore QtGui QtXml QtXmlPatterns REQUIRED) +find_package(Qt4 COMPONENTS QtCore QtGui QtNetwork QtXml QtXmlPatterns REQUIRED) include(${QT_USE_FILE}) qt4_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index e05ebcdeb..c339d8769 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -2,6 +2,8 @@ #include "editor.hpp" #include +#include +#include #include "model/doc/document.hpp" #include "model/world/data.hpp" @@ -114,6 +116,15 @@ void CS::Editor::createNewFile() mFileDialog.hide(); } +bool CS::Editor::makeIPCServer() +{ + server = new QLocalServer(this); + if(server->listen("IPCServer")) + return true; + server->close(); + return false; +} + int CS::Editor::run() { mStartup.show(); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 380e434c2..2d50b68fe 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -1,6 +1,8 @@ #ifndef CS_EDITOR_H #define CS_EDITOR_H +#include + #include #ifndef Q_MOC_RUN #include @@ -35,6 +37,8 @@ namespace CS Editor(); + bool makeIPCServer(); + int run(); ///< \return error status @@ -45,6 +49,10 @@ namespace CS void loadDocument(); void openFiles(); void createNewFile(); + + private: + + QLocalServer *server; }; } diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index 7f6f9302e..3e40ee8cc 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -39,5 +39,10 @@ int main(int argc, char *argv[]) CS::Editor editor; + if(!editor.makeIPCServer()) + { + return 0; + } + return editor.run(); } From 563bd0b4302519b11a33fb5092fbddd66b567d6a Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Tue, 3 Sep 2013 04:12:19 -0500 Subject: [PATCH 158/194] [Feature #881] Raise OpenCS New Window After Re-execution The OpenCS startup window of the existing exist is raised when trying to start a new instance. This is done by the new instance connection to the existing instance's QLocalServer. Once the connection is established the existing instance raises the startup window. --- apps/opencs/editor.cpp | 24 +++++++++++++++++++++++- apps/opencs/editor.hpp | 9 ++++++++- apps/opencs/main.cpp | 1 + 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index c339d8769..79dc368b4 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -10,6 +10,8 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) { + ipcServerName = "IPCServer"; + connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); @@ -116,15 +118,35 @@ void CS::Editor::createNewFile() mFileDialog.hide(); } +void CS::Editor::showStartup() +{ + if(mStartup.isHidden()) + mStartup.show(); + mStartup.raise(); + mStartup.activateWindow(); +} + bool CS::Editor::makeIPCServer() { server = new QLocalServer(this); - if(server->listen("IPCServer")) + + if(server->listen(ipcServerName)) + { + connect(server, SIGNAL(newConnection()), this, SLOT(showStartup())); return true; + } + server->close(); return false; } +void CS::Editor::connectToIPCServer() +{ + clientToServerSocket = new QLocalSocket(this); + clientToServerSocket->connectToServer(ipcServerName); + clientToServerSocket->close(); +} + int CS::Editor::run() { mStartup.show(); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 2d50b68fe..258d5b18a 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -1,9 +1,11 @@ #ifndef CS_EDITOR_H #define CS_EDITOR_H +#include +#include #include +#include -#include #ifndef Q_MOC_RUN #include #endif @@ -38,6 +40,7 @@ namespace CS Editor(); bool makeIPCServer(); + void connectToIPCServer(); int run(); ///< \return error status @@ -50,9 +53,13 @@ namespace CS void openFiles(); void createNewFile(); + void showStartup(); + private: + QString ipcServerName; QLocalServer *server; + QLocalSocket *clientToServerSocket; }; } diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index 3e40ee8cc..eddeb1983 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -41,6 +41,7 @@ int main(int argc, char *argv[]) if(!editor.makeIPCServer()) { + editor.connectToIPCServer(); return 0; } From 0ae2bb2fae55c48b885546ec0791b6730b5f7215 Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Tue, 3 Sep 2013 05:22:48 -0500 Subject: [PATCH 159/194] [Feature #881] Code Formatting Code formatted to the OpenMW policy. --- apps/opencs/editor.cpp | 16 ++++++++-------- apps/opencs/editor.hpp | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 79dc368b4..85303fb26 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -10,7 +10,7 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) { - ipcServerName = "IPCServer"; + mIpcServerName = "IPCServer"; connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); @@ -128,23 +128,23 @@ void CS::Editor::showStartup() bool CS::Editor::makeIPCServer() { - server = new QLocalServer(this); + mServer = new QLocalServer(this); - if(server->listen(ipcServerName)) + if(mServer->listen(mIpcServerName)) { - connect(server, SIGNAL(newConnection()), this, SLOT(showStartup())); + connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup())); return true; } - server->close(); + mServer->close(); return false; } void CS::Editor::connectToIPCServer() { - clientToServerSocket = new QLocalSocket(this); - clientToServerSocket->connectToServer(ipcServerName); - clientToServerSocket->close(); + mClientSocket = new QLocalSocket(this); + mClientSocket->connectToServer(mIpcServerName); + mClientSocket->close(); } int CS::Editor::run() diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 258d5b18a..80336d66f 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -57,9 +57,9 @@ namespace CS private: - QString ipcServerName; - QLocalServer *server; - QLocalSocket *clientToServerSocket; + QString mIpcServerName; + QLocalServer *mServer; + QLocalSocket *mClientSocket; }; } From 1744a64f77ac1740df6338d4657b66c15dd5fd11 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Sep 2013 12:32:06 +0200 Subject: [PATCH 160/194] in filters allow specifiying boolean columns as strings --- apps/opencs/model/filter/parser.cpp | 2 +- apps/opencs/model/filter/textnode.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index d334a7f63..15cdbfce2 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -120,7 +120,7 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() } if (string[0]=='"') - string = string.substr (1, string.size()-2); + return string.substr (1, string.size()-2); } return checkKeywords (string); diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index a826a2d54..f3d98ce53 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -45,6 +45,10 @@ bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, if (value>=0 && value (enums.size())) string = QString::fromUtf8 (enums[value].c_str()); } + else if (data.type()==QVariant::Bool) + { + string = data.toBool() ? "true" : " false"; + } else return false; From f80373a8494cf0720a192ff634aee98842f2e2d1 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Sep 2013 12:53:45 +0200 Subject: [PATCH 161/194] removed two unused files --- .../view/world/refrecordtypedelegate.cpp | 25 -------- .../view/world/refrecordtypedelegate.hpp | 58 ------------------- 2 files changed, 83 deletions(-) delete mode 100644 apps/opencs/view/world/refrecordtypedelegate.cpp delete mode 100644 apps/opencs/view/world/refrecordtypedelegate.hpp diff --git a/apps/opencs/view/world/refrecordtypedelegate.cpp b/apps/opencs/view/world/refrecordtypedelegate.cpp deleted file mode 100644 index 2bcb7ca50..000000000 --- a/apps/opencs/view/world/refrecordtypedelegate.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "refrecordtypedelegate.hpp" -#include "../../model/world/universalid.hpp" - -CSVWorld::RefRecordTypeDelegate::RefRecordTypeDelegate - (const std::vector > &values, QUndoStack& undoStack, QObject *parent) - : EnumDelegate (values, undoStack, parent) -{} - -CSVWorld::RefRecordTypeDelegateFactory::RefRecordTypeDelegateFactory() -{ - unsigned int argSize = CSMWorld::UniversalId::getIdArgSize(); - - for (unsigned int i = 0; i < argSize; i++) - { - std::pair idPair = CSMWorld::UniversalId::getIdArgPair(i); - - mValues.push_back (std::pair(idPair.first, QString::fromUtf8(idPair.second))); - } -} - -CSVWorld::CommandDelegate *CSVWorld::RefRecordTypeDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const -{ - return new RefRecordTypeDelegate (mValues, undoStack, parent); -} diff --git a/apps/opencs/view/world/refrecordtypedelegate.hpp b/apps/opencs/view/world/refrecordtypedelegate.hpp deleted file mode 100644 index baec2cc2e..000000000 --- a/apps/opencs/view/world/refrecordtypedelegate.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef REFRECORDTYPEDELEGATE_HPP -#define REFRECORDTYPEDELEGATE_HPP - -#include "enumdelegate.hpp" -#include "util.hpp" - -namespace CSVWorld -{ - class RefRecordTypeDelegate : public EnumDelegate - { - public: - RefRecordTypeDelegate (const std::vector > &mValues, QUndoStack& undoStack, QObject *parent); - }; - - class RefRecordTypeDelegateFactory : public CommandDelegateFactory - { - - std::vector > mValues; - - public: - RefRecordTypeDelegateFactory(); - - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; - ///< The ownership of the returned CommandDelegate is transferred to the caller. - }; -} -/* - class VarTypeDelegate : public EnumDelegate - { - private: - - virtual void addCommands (QAbstractItemModel *model, - const QModelIndex& index, int type) const; - - public: - - VarTypeDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent); - }; - - class VarTypeDelegateFactory : public CommandDelegateFactory - { - std::vector > mValues; - - public: - - VarTypeDelegateFactory (ESM::VarType type0 = ESM::VT_Unknown, - ESM::VarType type1 = ESM::VT_Unknown, ESM::VarType type2 = ESM::VT_Unknown, - ESM::VarType type3 = ESM::VT_Unknown); - - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; - ///< The ownership of the returned CommandDelegate is transferred to the caller. - - void add (ESM::VarType type); - }; -*/ - -#endif // REFRECORDTYPEDELEGATE_HPP From 80f8024da790b8f2490c037df317f8308fbc0f93 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Sep 2013 13:03:02 +0200 Subject: [PATCH 162/194] allow specifying record modification status column enum via strings --- apps/opencs/model/world/columns.cpp | 6 ++++++ .../opencs/view/world/recordstatusdelegate.cpp | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 73a58ac82..2a47a73fb 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -245,6 +245,11 @@ namespace "Bolt", 0 }; + static const char *sModificationEnums[] = + { + "Base", "Modified", "Added", "Deleted", "Deleted", 0 + }; + const char **getEnumNames (CSMWorld::Columns::ColumnId column) { switch (column) @@ -257,6 +262,7 @@ namespace case CSMWorld::Columns::ColumnId_ClothingType: return sClothingTypes; case CSMWorld::Columns::ColumnId_CreatureType: return sCreatureTypes; case CSMWorld::Columns::ColumnId_WeaponType: return sWeaponTypes; + case CSMWorld::Columns::ColumnId_Modification: return sModificationEnums; default: return 0; } diff --git a/apps/opencs/view/world/recordstatusdelegate.cpp b/apps/opencs/view/world/recordstatusdelegate.cpp index a0ffd3063..8085ec7be 100644 --- a/apps/opencs/view/world/recordstatusdelegate.cpp +++ b/apps/opencs/view/world/recordstatusdelegate.cpp @@ -1,8 +1,11 @@ #include "recordstatusdelegate.hpp" + #include #include #include + #include "../../model/settings/usersettings.hpp" +#include "../../model/world/columns.hpp" CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, const IconList & icons, @@ -37,9 +40,14 @@ bool CSVWorld::RecordStatusDelegate::updateEditorSetting (const QString &setting CSVWorld::RecordStatusDelegateFactory::RecordStatusDelegateFactory() { - DataDisplayDelegateFactory::add ( CSMWorld::RecordBase::State_BaseOnly, "Base", ":./base.png"); - DataDisplayDelegateFactory::add ( CSMWorld::RecordBase::State_Deleted, "Deleted", ":./removed.png"); - DataDisplayDelegateFactory::add ( CSMWorld::RecordBase::State_Erased, "Deleted", ":./removed.png"); - DataDisplayDelegateFactory::add ( CSMWorld::RecordBase::State_Modified, "Modified", ":./modified.png"); - DataDisplayDelegateFactory::add ( CSMWorld::RecordBase::State_ModifiedOnly, "Added", ":./added.png"); + std::vector enums = + CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); + + static const char *sIcons[] = + { + ":./base.png", ":./modified.png", ":./added.png", ":./removed.png", ":./removed.png", 0 + }; + + for (int i=0; sIcons[i]; ++i) + add (i, enums.at (i).c_str(), sIcons[i]); } From 6b11f59ed4e51a2f81f606df2a435680a9d990d6 Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Tue, 3 Sep 2013 06:24:11 -0500 Subject: [PATCH 163/194] [Feature #881] Rename QLocalServer The server name should be more unique to the project so there isn't a interprocess clash. --- apps/opencs/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 85303fb26..36d4f9735 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -10,7 +10,7 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) { - mIpcServerName = "IPCServer"; + mIpcServerName = "org.openmw.OpenCS"; connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); From 842a616909fee9e1c79d78789125e629d095354f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Sep 2013 14:18:34 +0200 Subject: [PATCH 164/194] more cleanup --- apps/opencs/model/world/universalid.cpp | 21 ---------- apps/opencs/model/world/universalid.hpp | 3 -- apps/opencs/view/world/refidtypedelegate.cpp | 40 ++++++++++---------- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 42ebd1f80..1cc4ca3c4 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -90,8 +90,6 @@ namespace { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; - - static const unsigned int IDARG_SIZE = sizeof (sIdArg) / sizeof (TypeData); } CSMWorld::UniversalId::UniversalId (const std::string& universalId) @@ -293,25 +291,6 @@ std::vector CSMWorld::UniversalId::listReferenceabl return list; } -std::pair CSMWorld::UniversalId::getIdArgPair (unsigned int index) -{ - std::pair retPair; - - if ( index < IDARG_SIZE ) - { - retPair.first = sIdArg[index].mType; - retPair.second = sIdArg[index].mName; - } - - return retPair; -} - -unsigned int CSMWorld::UniversalId::getIdArgSize() -{ - return IDARG_SIZE; -} - - bool CSMWorld::operator== (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { return left.isEqual (right); diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 8042c3dfd..341b941e6 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -134,9 +134,6 @@ namespace CSMWorld ///< Will return an empty string, if no icon is available. static std::vector listReferenceableTypes(); - - static std::pair getIdArgPair (unsigned int index); - static unsigned int getIdArgSize (); }; bool operator== (const UniversalId& left, const UniversalId& right); diff --git a/apps/opencs/view/world/refidtypedelegate.cpp b/apps/opencs/view/world/refidtypedelegate.cpp index bf3acbb20..7cffbf3dd 100755 --- a/apps/opencs/view/world/refidtypedelegate.cpp +++ b/apps/opencs/view/world/refidtypedelegate.cpp @@ -1,4 +1,5 @@ #include "refidtypedelegate.hpp" + #include "../../model/world/universalid.hpp" CSVWorld::RefIdTypeDelegate::RefIdTypeDelegate @@ -6,6 +7,26 @@ CSVWorld::RefIdTypeDelegate::RefIdTypeDelegate : DataDisplayDelegate (values, icons, undoStack, parent) {} +bool CSVWorld::RefIdTypeDelegate::updateEditorSetting (const QString &settingName, const QString &settingValue) +{ + if (settingName == "Referenceable ID Type Display") + { + if (settingValue == "Icon and Text") + mDisplayMode = Mode_IconAndText; + + else if (settingValue == "Icon Only") + mDisplayMode = Mode_IconOnly; + + else if (settingValue == "Text Only") + mDisplayMode = Mode_TextOnly; + + return true; + } + + return false; +} + + CSVWorld::RefIdTypeDelegateFactory::RefIdTypeDelegateFactory() { UidTypeList uIdList = buildUidTypeList(); @@ -39,22 +60,3 @@ CSVWorld::RefIdTypeDelegateFactory::UidTypeList CSVWorld::RefIdTypeDelegateFacto return list; } - -bool CSVWorld::RefIdTypeDelegate::updateEditorSetting (const QString &settingName, const QString &settingValue) -{ - if (settingName == "Referenceable ID Type Display") - { - if (settingValue == "Icon and Text") - mDisplayMode = Mode_IconAndText; - - else if (settingValue == "Icon Only") - mDisplayMode = Mode_IconOnly; - - else if (settingValue == "Text Only") - mDisplayMode = Mode_TextOnly; - - return true; - } - - return false; -} From 51652047d0f08390b88a0e49af92b9ca13b59fe0 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Sep 2013 14:27:53 +0200 Subject: [PATCH 165/194] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index dd583a0ec..9a84c5327 100644 --- a/credits.txt +++ b/credits.txt @@ -43,6 +43,7 @@ lazydev Leon Saunders (emoose) Lukasz Gromanowski (lgro) Manuel Edelmann (vorenon) +Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) Mateusz Kołaczek (PL_kolek) From 22a5f7198f8fd893fcf9bb590ef4aafb7a454593 Mon Sep 17 00:00:00 2001 From: "Alexander \"Ace\" Olofsson" Date: Wed, 4 Sep 2013 23:25:47 +0200 Subject: [PATCH 166/194] A few fixes to build on Windows --- apps/launcher/unshieldthread.hpp | 5 ++++- apps/opencs/model/world/refcollection.cpp | 2 ++ apps/opencs/model/world/refcollection.hpp | 4 +++- libs/openengine/ogre/lights.cpp | 10 +++++----- libs/openengine/ogre/lights.hpp | 15 +++++++++++++-- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/launcher/unshieldthread.hpp b/apps/launcher/unshieldthread.hpp index b48d3d987..c4e15cccd 100644 --- a/apps/launcher/unshieldthread.hpp +++ b/apps/launcher/unshieldthread.hpp @@ -5,8 +5,9 @@ #include +#ifndef _WIN32 #include - +#endif class UnshieldThread : public QThread { @@ -33,7 +34,9 @@ class UnshieldThread : public QThread private: void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); +#ifndef _WIN32 bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); +#endif boost::filesystem::path mMorrowindPath; boost::filesystem::path mTribunalPath; diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 085817753..f06cfb545 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -6,9 +6,11 @@ #include "ref.hpp" #include "cell.hpp" +/* CSMWorld::RefCollection::RefCollection (Collection& cells) : mCells (cells), mNextId (0) {} +*/ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base) { diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index 895315a17..c22f17d7b 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -17,7 +17,9 @@ namespace CSMWorld public: - RefCollection (Collection& cells); + RefCollection (Collection& cells) + : mCells (cells), mNextId (0) + {} void load (ESM::ESMReader& reader, int cellIndex, bool base); ///< Load a sequence of references. diff --git a/libs/openengine/ogre/lights.cpp b/libs/openengine/ogre/lights.cpp index 52aca6a70..ef16e0b2f 100644 --- a/libs/openengine/ogre/lights.cpp +++ b/libs/openengine/ogre/lights.cpp @@ -1,12 +1,12 @@ #include "lights.hpp" #include -#include + namespace OEngine { namespace Render { - +/* LightFunction::LightFunction(LightType type) : ControllerFunction(true) , mType(type) @@ -14,7 +14,7 @@ LightFunction::LightFunction(LightType type) , mDirection(1.0f) { } - +*/ Ogre::Real LightFunction::pulseAmplitude(Ogre::Real time) { return std::sin(time); @@ -97,13 +97,13 @@ Ogre::Real LightFunction::calculate(Ogre::Real value) return brightness; } - +/* LightValue::LightValue(Ogre::Light *light, const Ogre::ColourValue &color) : mTarget(light) , mColor(color) { } - +*/ Ogre::Real LightValue::getValue() const { return 0.0f; diff --git a/libs/openengine/ogre/lights.hpp b/libs/openengine/ogre/lights.hpp index c63f16425..46b3ff1d0 100644 --- a/libs/openengine/ogre/lights.hpp +++ b/libs/openengine/ogre/lights.hpp @@ -3,6 +3,7 @@ #include #include +#include /* * Controller classes to handle pulsing and flicker lights @@ -30,7 +31,13 @@ namespace Render { static Ogre::Real flickerFrequency(Ogre::Real phase); public: - LightFunction(LightType type); + LightFunction(LightType type) + : ControllerFunction(true) + , mType(type) + , mPhase(Ogre::Math::RangeRandom(-500.0f, +500.0f)) + , mDirection(1.0f) + { + } virtual Ogre::Real calculate(Ogre::Real value); }; @@ -40,7 +47,11 @@ namespace Render { Ogre::ColourValue mColor; public: - LightValue(Ogre::Light *light, const Ogre::ColourValue &color); + LightValue(Ogre::Light *light, const Ogre::ColourValue &color) + : mTarget(light) + , mColor(color) + { + } virtual Ogre::Real getValue() const; virtual void setValue(Ogre::Real value); From 5e42c73356a684b0f1bc92e032418581cc65162a Mon Sep 17 00:00:00 2001 From: "Alexander \"Ace\" Olofsson" Date: Thu, 5 Sep 2013 10:39:17 +0200 Subject: [PATCH 167/194] Better fix for unshield, comments on changes, no more commented out code. --- apps/launcher/CMakeLists.txt | 2 -- apps/launcher/unshieldthread.hpp | 4 ---- apps/opencs/model/world/refcollection.cpp | 6 ------ apps/opencs/model/world/refcollection.hpp | 2 +- libs/openengine/ogre/lights.cpp | 16 ---------------- libs/openengine/ogre/lights.hpp | 2 ++ 6 files changed, 3 insertions(+), 29 deletions(-) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 5908deb90..92cabffff 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -24,7 +24,6 @@ set(LAUNCHER_HEADER graphicspage.hpp maindialog.hpp playpage.hpp - unshieldthread.hpp textslotmsgbox.hpp settings/gamesettings.hpp @@ -47,7 +46,6 @@ set(LAUNCHER_HEADER_MOC graphicspage.hpp maindialog.hpp playpage.hpp - unshieldthread.hpp textslotmsgbox.hpp utils/checkablemessagebox.hpp diff --git a/apps/launcher/unshieldthread.hpp b/apps/launcher/unshieldthread.hpp index c4e15cccd..655cb5b53 100644 --- a/apps/launcher/unshieldthread.hpp +++ b/apps/launcher/unshieldthread.hpp @@ -5,9 +5,7 @@ #include -#ifndef _WIN32 #include -#endif class UnshieldThread : public QThread { @@ -34,9 +32,7 @@ class UnshieldThread : public QThread private: void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); -#ifndef _WIN32 bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); -#endif boost::filesystem::path mMorrowindPath; boost::filesystem::path mTribunalPath; diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index f06cfb545..696aeefaa 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -6,12 +6,6 @@ #include "ref.hpp" #include "cell.hpp" -/* -CSMWorld::RefCollection::RefCollection (Collection& cells) -: mCells (cells), mNextId (0) -{} -*/ - void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base) { Record cell = mCells.getRecord (cellIndex); diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index c22f17d7b..b5f8c8064 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -16,7 +16,7 @@ namespace CSMWorld int mNextId; public: - + // MSVC needs the constructor for a class inheriting a template to be defined in header RefCollection (Collection& cells) : mCells (cells), mNextId (0) {} diff --git a/libs/openengine/ogre/lights.cpp b/libs/openengine/ogre/lights.cpp index ef16e0b2f..348057b84 100644 --- a/libs/openengine/ogre/lights.cpp +++ b/libs/openengine/ogre/lights.cpp @@ -6,15 +6,6 @@ namespace OEngine { namespace Render { -/* -LightFunction::LightFunction(LightType type) - : ControllerFunction(true) - , mType(type) - , mPhase(Ogre::Math::RangeRandom(-500.0f, +500.0f)) - , mDirection(1.0f) -{ -} -*/ Ogre::Real LightFunction::pulseAmplitude(Ogre::Real time) { return std::sin(time); @@ -97,13 +88,6 @@ Ogre::Real LightFunction::calculate(Ogre::Real value) return brightness; } -/* -LightValue::LightValue(Ogre::Light *light, const Ogre::ColourValue &color) - : mTarget(light) - , mColor(color) -{ -} -*/ Ogre::Real LightValue::getValue() const { return 0.0f; diff --git a/libs/openengine/ogre/lights.hpp b/libs/openengine/ogre/lights.hpp index 46b3ff1d0..61d09a0e6 100644 --- a/libs/openengine/ogre/lights.hpp +++ b/libs/openengine/ogre/lights.hpp @@ -31,6 +31,7 @@ namespace Render { static Ogre::Real flickerFrequency(Ogre::Real phase); public: + // MSVC needs the constructor for a class inheriting a template to be defined in header LightFunction(LightType type) : ControllerFunction(true) , mType(type) @@ -47,6 +48,7 @@ namespace Render { Ogre::ColourValue mColor; public: + // MSVC needs the constructor for a class inheriting a template to be defined in header LightValue(Ogre::Light *light, const Ogre::ColourValue &color) : mTarget(light) , mColor(color) From 8bebe7179cb08cd750eef2020e01a0d3a2dadfc8 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 13:58:36 +0200 Subject: [PATCH 168/194] some UniversalId fixes --- apps/opencs/model/world/universalid.cpp | 18 +++++++++++++++++- apps/opencs/model/world/universalid.hpp | 5 +++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 1cc4ca3c4..d360fde8f 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -80,7 +80,7 @@ namespace { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":./static.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":./weapon.png" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Reference", 0 }, - + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Filter, "Filter", 0 }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; @@ -149,6 +149,22 @@ CSMWorld::UniversalId::UniversalId (Type type) : mArgumentType (ArgumentType_Non return; } + for (int i=0; sIdArg[i].mName; ++i) + if (type==sIdArg[i].mType) + { + mArgumentType = ArgumentType_Id; + mClass = sIdArg[i].mClass; + return; + } + + for (int i=0; sIndexArg[i].mName; ++i) + if (type==sIndexArg[i].mType) + { + mArgumentType = ArgumentType_Index; + mClass = sIndexArg[i].mClass; + return; + } + throw std::logic_error ("invalid argument-less UniversalId type"); } diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 341b941e6..aa0cdacc0 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -34,7 +34,7 @@ namespace CSMWorld enum Type { - Type_None, + Type_None = 0, Type_Globals, Type_Global, Type_VerificationResults, @@ -89,6 +89,8 @@ namespace CSMWorld Type_Filters }; + enum { NumberOfTypes = Type_Filters+1 }; + private: Class mClass; @@ -102,7 +104,6 @@ namespace CSMWorld UniversalId (const std::string& universalId); UniversalId (Type type = Type_None); - ///< Using a type for a non-argument-less UniversalId will throw an exception. UniversalId (Type type, const std::string& id); ///< Using a type for a non-ID-argument UniversalId will throw an exception. From f2e86e860e4100e1c15b6f6eef8850f6e99bcaea Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 13:59:09 +0200 Subject: [PATCH 169/194] allow the use of record types in string filters --- apps/opencs/model/world/columns.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 2a47a73fb..80525b59e 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -3,6 +3,8 @@ #include +#include "universalid.hpp" + namespace CSMWorld { namespace Columns @@ -271,7 +273,7 @@ namespace bool CSMWorld::Columns::hasEnums (ColumnId column) { - return getEnumNames (column)!=0; + return getEnumNames (column)!=0 || column==ColumnId_RecordType; } std::vector CSMWorld::Columns::getEnums (ColumnId column) @@ -281,6 +283,13 @@ std::vector CSMWorld::Columns::getEnums (ColumnId column) if (const char **table = getEnumNames (column)) for (int i=0; table[i]; ++i) enums.push_back (table[i]); + else if (column==ColumnId_RecordType) + { + enums.push_back (""); // none + + for (int i=UniversalId::Type_None+1; i (i)).getTypeName()); + } return enums; } \ No newline at end of file From 9332684335befac65612392b99a30b6adf5f73df Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 15:05:49 +0200 Subject: [PATCH 170/194] allow the use of value types in string filters --- apps/opencs/model/world/columns.cpp | 6 +++++ apps/opencs/view/world/vartypedelegate.cpp | 31 +++++----------------- components/esm/variant.hpp | 2 +- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 80525b59e..5616a4a48 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -252,6 +252,11 @@ namespace "Base", "Modified", "Added", "Deleted", "Deleted", 0 }; + static const char *sVarTypeEnums[] = + { + "unknown", "none", "short", "integer", "long", "float", "string", 0 + }; + const char **getEnumNames (CSMWorld::Columns::ColumnId column) { switch (column) @@ -265,6 +270,7 @@ namespace case CSMWorld::Columns::ColumnId_CreatureType: return sCreatureTypes; case CSMWorld::Columns::ColumnId_WeaponType: return sWeaponTypes; case CSMWorld::Columns::ColumnId_Modification: return sModificationEnums; + case CSMWorld::Columns::ColumnId_ValueType: return sVarTypeEnums; default: return 0; } diff --git a/apps/opencs/view/world/vartypedelegate.cpp b/apps/opencs/view/world/vartypedelegate.cpp index 72cbaae42..15ce2dbaf 100644 --- a/apps/opencs/view/world/vartypedelegate.cpp +++ b/apps/opencs/view/world/vartypedelegate.cpp @@ -4,6 +4,7 @@ #include #include "../../model/world/commands.hpp" +#include "../../model/world/columns.hpp" void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) const @@ -75,29 +76,11 @@ CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate (QUndo void CSVWorld::VarTypeDelegateFactory::add (ESM::VarType type) { - struct Name - { - ESM::VarType mType; - const char *mName; - }; + std::vector enums = + CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_ValueType); - static const Name sNames[] = - { - { ESM::VT_None, "empty" }, - { ESM::VT_Short, "short" }, - { ESM::VT_Int, "integer" }, - { ESM::VT_Long, "long" }, - { ESM::VT_Float, "float" }, - { ESM::VT_String, "string" }, - { ESM::VT_Unknown, 0 } // end marker - }; - - for (int i=0; sNames[i].mName; ++i) - if (sNames[i].mType==type) - { - mValues.push_back (std::make_pair (type, sNames[i].mName)); - return; - } - - throw std::logic_error ("Unsupported variable type"); + if (type<0 && type>=enums.size()) + throw std::logic_error ("Unsupported variable type"); + + mValues.push_back (std::make_pair (type, QString::fromUtf8 (enums[type].c_str()))); } diff --git a/components/esm/variant.hpp b/components/esm/variant.hpp index 8c5f3b3d4..2bba60a15 100644 --- a/components/esm/variant.hpp +++ b/components/esm/variant.hpp @@ -11,7 +11,7 @@ namespace ESM enum VarType { - VT_Unknown, + VT_Unknown = 0, VT_None, VT_Short, // stored as a float, kinda VT_Int, From f3ce9c22a1a6439f3b6bb489cf20658c7934bc0a Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 15:22:14 +0200 Subject: [PATCH 171/194] rewrote value node to support half-sided intervals (meaning infinity or -infinity for the missing half) --- apps/opencs/model/filter/parser.cpp | 12 +++---- apps/opencs/model/filter/valuenode.cpp | 46 ++++++++++++++++++++------ apps/opencs/model/filter/valuenode.hpp | 15 +++++++-- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 15cdbfce2..5b1679cdd 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -437,22 +437,22 @@ boost::shared_ptr CSMFilter::Parser::parseValue() // parse value double lower = 0; double upper = 0; - bool min = false; - bool max = false; + ValueNode::Type lowerType = ValueNode::Type_Open; + ValueNode::Type upperType = ValueNode::Type_Open; token = getNextToken(); if (token.mType==Token::Type_Number) { // single value - min = max = true; lower = upper = token.mNumber; + lowerType = upperType = ValueNode::Type_Closed; } else { // interval if (token.mType==Token::Type_OpenSquare) - min = true; + lowerType = ValueNode::Type_Closed; else if (token.mType!=Token::Type_CloseSquare && token.mType!=Token::Type_Open) { error(); @@ -490,7 +490,7 @@ boost::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); if (token.mType==Token::Type_CloseSquare) - max = true; + upperType = ValueNode::Type_Closed; else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) { error(); @@ -506,7 +506,7 @@ boost::shared_ptr CSMFilter::Parser::parseValue() return boost::shared_ptr(); } - return boost::shared_ptr (new ValueNode (columnId, lower, upper, min, max)); + return boost::shared_ptr (new ValueNode (columnId, lowerType, upperType, lower, upper)); } void CSMFilter::Parser::error() diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index f6cb20e4c..7eeb6beab 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -7,10 +7,9 @@ #include "../world/columns.hpp" #include "../world/idtable.hpp" -CSMFilter::ValueNode::ValueNode (int columnId, - double lower, double upper, bool min, bool max) -: mColumnId (columnId), mLower (lower), mUpper (upper), mMin (min), mMax (max) -{} +CSMFilter::ValueNode::ValueNode (int columnId, Type lowerType, Type upperType, + double lower, double upper) +: mColumnId (columnId), mLowerType (lowerType), mUpperType (upperType), mLower (lower), mUpper (upper){} bool CSMFilter::ValueNode::test (const CSMWorld::IdTable& table, int row, const std::map& columns) const @@ -33,10 +32,21 @@ bool CSMFilter::ValueNode::test (const CSMWorld::IdTable& table, int row, double value = data.toDouble(); - if (mLower==mUpper && mMin && mMax) - return value==mLower; - - return (mMin ? value>=mLower : value>mLower) && (mMax ? value<=mUpper : valuemUpper) return false; break; + case Type_Open: if (value>=mUpper) return false; break; + case Type_Infinite: break; + } + + return true; } std::vector CSMFilter::ValueNode::getReferencedColumns() const @@ -60,10 +70,26 @@ std::string CSMFilter::ValueNode::toString (bool numericColumns) const stream << ", \""; - if (mLower==mUpper && mMin && mMax) + if (mLower==mUpper && mLowerType!=Type_Infinite && mUpperType!=Type_Infinite) stream << mLower; else - stream << (mMin ? "[" : "(") << mLower << ", " << mUpper << (mMax ? "]" : ")"); + { + switch (mLowerType) + { + case Type_Closed: stream << "[" << mLower; break; + case Type_Open: stream << "(" << mLower; break; + case Type_Infinite: stream << "("; break; + } + + stream << ", "; + + switch (mUpperType) + { + case Type_Closed: stream << mUpper << "]"; break; + case Type_Open: stream << mUpper << ")"; break; + case Type_Infinite: stream << ")"; break; + } + } stream << ")"; diff --git a/apps/opencs/model/filter/valuenode.hpp b/apps/opencs/model/filter/valuenode.hpp index faaa1e2ff..b1050709d 100644 --- a/apps/opencs/model/filter/valuenode.hpp +++ b/apps/opencs/model/filter/valuenode.hpp @@ -7,16 +7,25 @@ namespace CSMFilter { class ValueNode : public LeafNode { + public: + + enum Type + { + Type_Closed, Type_Open, Type_Infinite + }; + + private: + int mColumnId; std::string mText; double mLower; double mUpper; - bool mMin; - bool mMax; + Type mLowerType; + Type mUpperType; public: - ValueNode (int columnId, double lower, double upper, bool min, bool max); + ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper); virtual bool test (const CSMWorld::IdTable& table, int row, const std::map& columns) const; From 645b50ef36150f47d799919e5353cddd67670798 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 15:38:28 +0200 Subject: [PATCH 172/194] added support for half-sided intervals to filter parser --- apps/opencs/model/filter/parser.cpp | 40 +++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 5b1679cdd..8567e0a95 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -461,17 +461,23 @@ boost::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType!=Token::Type_Number) + if (token.mType==Token::Type_Number) { - error(); - return boost::shared_ptr(); - } + lower = token.mNumber; - lower = token.mNumber; - - token = getNextToken(); + token = getNextToken(); - if (token.mType!=Token::Type_Comma) + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + } + else if (token.mType==Token::Type_Comma) + { + lowerType = ValueNode::Type_Infinite; + } + else { error(); return boost::shared_ptr(); @@ -479,18 +485,20 @@ boost::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType!=Token::Type_Number) + if (token.mType==Token::Type_Number) { - error(); - return boost::shared_ptr(); - } - - upper = token.mNumber; + upper = token.mNumber; - token = getNextToken(); + token = getNextToken(); + } + else + upperType = ValueNode::Type_Infinite; if (token.mType==Token::Type_CloseSquare) - upperType = ValueNode::Type_Closed; + { + if (upperType!=ValueNode::Type_Infinite) + upperType = ValueNode::Type_Closed; + } else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) { error(); From 8f0ab29a9f5d9c5d44dcbd5b50e82949e52fbe27 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 5 Sep 2013 15:47:38 +0200 Subject: [PATCH 173/194] allow the use of keywords for strings without quotation marks --- apps/opencs/model/filter/parser.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 8567e0a95..8f4fcb70c 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -49,19 +49,31 @@ namespace CSMFilter Token (Type type = Type_None); + Token (Type type, const std::string& string); + ///< Non-string type that can also be interpreted as a string. + Token (const std::string& string); Token (double number); operator bool() const; + + bool isString() const; }; Token::Token (Type type) : mType (type) {} + Token::Token (Type type, const std::string& string) : mType (type), mString (string) {} + Token::Token (const std::string& string) : mType (Type_String), mString (string) {} Token::Token (double number) : mType (Type_Number), mNumber (number) {} + bool Token::isString() const + { + return mType==Type_String || mType>=Type_Keyword_True; + } + Token::operator bool() const { return mType!=Type_None; @@ -182,7 +194,7 @@ CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) for (int i=0; sKeywords[i]; ++i) if (sKeywords[i]==string || (string.size()==1 && sKeywords[i][0]==string[0])) - return Token (static_cast (i+Token::Type_Keyword_True)); + return Token (static_cast (i+Token::Type_Keyword_True), token.mString); return token; } @@ -351,7 +363,7 @@ boost::shared_ptr CSMFilter::Parser::parseText() if (static_cast (token.mNumber)==token.mNumber) columnId = static_cast (token.mNumber); } - else if (token.mType==Token::Type_String) + else if (token.isString()) { columnId = CSMWorld::Columns::getId (token.mString); } @@ -373,7 +385,7 @@ boost::shared_ptr CSMFilter::Parser::parseText() // parse text pattern token = getNextToken(); - if (token.mType!=Token::Type_String) + if (!token.isString()) { error(); return boost::shared_ptr(); @@ -415,7 +427,7 @@ boost::shared_ptr CSMFilter::Parser::parseValue() if (static_cast (token.mNumber)==token.mNumber) columnId = static_cast (token.mNumber); } - else if (token.mType==Token::Type_String) + else if (token.isString()) { columnId = CSMWorld::Columns::getId (token.mString); } @@ -561,6 +573,8 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) return true; } + // We do not use isString() here, because there could be a pre-defined filter with an ID that is + // equal a filter keyword. else if (token.mType==Token::Type_String && allowPredefined) { if (getNextToken()!=Token (Token::Type_EOS)) From 784cba21f21b4acd5461974b2163faca77a4c83f Mon Sep 17 00:00:00 2001 From: Marc Bouvier Date: Thu, 5 Sep 2013 23:02:51 -0500 Subject: [PATCH 174/194] [Feature #881] Change Tabs to Spaces Some of the tabs were not made of spaces, so Git defaults tabs to the length of 8 spaces. OpenMW uses 4 spaces per tab. --- apps/opencs/editor.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 36d4f9735..3f6944a8c 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -10,7 +10,7 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) { - mIpcServerName = "org.openmw.OpenCS"; + mIpcServerName = "org.openmw.OpenCS"; connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); @@ -120,31 +120,31 @@ void CS::Editor::createNewFile() void CS::Editor::showStartup() { - if(mStartup.isHidden()) - mStartup.show(); - mStartup.raise(); - mStartup.activateWindow(); + if(mStartup.isHidden()) + mStartup.show(); + mStartup.raise(); + mStartup.activateWindow(); } bool CS::Editor::makeIPCServer() { - mServer = new QLocalServer(this); + mServer = new QLocalServer(this); - if(mServer->listen(mIpcServerName)) - { - connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup())); - return true; - } + if(mServer->listen(mIpcServerName)) + { + connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup())); + return true; + } - mServer->close(); - return false; + mServer->close(); + return false; } void CS::Editor::connectToIPCServer() { - mClientSocket = new QLocalSocket(this); - mClientSocket->connectToServer(mIpcServerName); - mClientSocket->close(); + mClientSocket = new QLocalSocket(this); + mClientSocket->connectToServer(mIpcServerName); + mClientSocket->close(); } int CS::Editor::run() From 5b90343c55b43b7909f388997d38cb69370e79db Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 6 Sep 2013 10:49:35 +0200 Subject: [PATCH 175/194] updated changelog --- readme.txt | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/readme.txt b/readme.txt index ade5da04d..7865f8dba 100644 --- a/readme.txt +++ b/readme.txt @@ -82,6 +82,50 @@ Allowed options: CHANGELOG +0.26.0 + +Bug #274: Inconsistencies in the terrain +Bug #557: Already-dead NPCs do not equip clothing/items. +Bug #592: Window resizing +Bug #612: [Tamriel Rebuilt] Missing terrain (South of Tel Oren) +Bug #664: Heart of lorkhan acts like a dead body (container) +Bug #767: Wonky ramp physics & water +Bug #780: Swimming out of water +Bug #792: Wrong ground alignment on actors when no clipping +Bug #796: Opening and closing door sound issue +Bug #797: No clipping hinders opening and closing of doors +Bug #799: sliders in enchanting window +Bug #838: Pressing key during startup procedure freezes the game +Bug #839: Combat/magic stances during character creation +Bug #843: [Tribunal] Dark Brotherhood assassin appears without equipment +Bug #844: Resting "until healed" option given even with full stats +Bug #846: Equipped torches are invisible. +Bug #847: Incorrect formula for autocalculated NPC initial health +Bug #850: Shealt weapon sound plays when leaving magic-ready stance +Bug #852: Some boots do not produce footstep sounds +Bug #860: FPS bar misalignment +Bug #861: Unable to print screen +Bug #863: No sneaking and jumping at the same time +Bug #866: Empty variables in [Movies] section of Morrowind.ini gets imported into OpenMW.cfg as blank fallback option and crashes game on start. +Bug #867: Dancing girls in "Suran, Desele's House of Earthly Delights" don't dance. +Bug #868: Idle animations are repeated +Bug #874: Underwater swimming close to the ground is jerky +Bug #875: Animation problem while swimming on the surface and looking up +Bug #876: Always a starting upper case letter in the inventory +Bug #878: Active spell effects don't update the layout properly when ended +Bug #891: Cell 24,-12 (Tamriel Rebuilt) crashes on load +Bug #896: New game sound issue +Feature #49: Melee Combat +Feature #71: Lycanthropy +Feature #393: Initialise MWMechanics::AiSequence from ESM::AIPackageList +Feature #622: Multiple positions for inventory window +Feature #627: Drowning +Feature #786: Allow the 'Activate' key to close the countdialog window +Feature #798: Morrowind installation via Launcher (Linux/Max OS only) +Feature #851: First/Third person transitions with mouse wheel +Task #689: change PhysicActor::enableCollisions +Task #707: Reorganise Compiler + 0.25.0 Bug #411: Launcher crash on OS X < 10.8 From 10c21579bc076774897d72f787f5a86ffa619082 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 7 Sep 2013 01:00:38 +0200 Subject: [PATCH 176/194] Item stacking case sensitivity fix --- apps/openmw/mwworld/containerstore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 9d111a525..c6768f5fd 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -80,7 +80,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() bool MWWorld::ContainerStore::stacks(const Ptr& ptr1, const Ptr& ptr2) { /// \todo add current enchantment charge here when it is implemented - if ( ptr1.getCellRef().mRefID == ptr2.getCellRef().mRefID + if ( Misc::StringUtils::ciEqual(ptr1.getCellRef().mRefID, ptr2.getCellRef().mRefID) && MWWorld::Class::get(ptr1).getScript(ptr1) == "" // item with a script never stacks && MWWorld::Class::get(ptr1).getEnchantment(ptr1) == "" // item with enchantment never stacks (we could revisit this later, but for now it makes selecting items in the spell window much easier) && ptr1.getCellRef().mOwner == ptr2.getCellRef().mOwner From 5a792b10b9d0518e8ace56d8a4c032b2eaa81a4f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 7 Sep 2013 09:30:40 +0200 Subject: [PATCH 177/194] fixed local openmw.cfg file --- files/openmw.cfg.local | 3 +++ 1 file changed, 3 insertions(+) diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local index dd116e108..d6ca2d554 100644 --- a/files/openmw.cfg.local +++ b/files/openmw.cfg.local @@ -1,2 +1,5 @@ +data="?global?data" +data="?mw?Data Files" data=./data +data-local="?user?data" resources=./resources From 2b50e3456662a7434706b3a1eca01069cba70b21 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sat, 7 Sep 2013 15:41:51 +0100 Subject: [PATCH 178/194] Fixed issue with copy_file from: https://forum.openmw.org/viewtopic.php?f=20&t=1718&hilit=0.26&start=50#p18705 --- apps/launcher/unshieldthread.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index ab9d984e1..ea293b29d 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -216,7 +216,12 @@ namespace else { if(copy) - bfs::copy_file(dir->path(), to / dir->path().filename()); + { + bfs::path dest = to / dir->path().filename(); + if(bfs::exists(dest)) + bfs::remove_all(dest); + bfs::copy_file(dir->path(), dest); + } else bfs::rename(dir->path(), to / dir->path().filename()); } From 7b9c5c75e70d54d5b6c49f6b1b7f9d0b8a858ac8 Mon Sep 17 00:00:00 2001 From: Tom Mason Date: Sat, 7 Sep 2013 16:16:09 +0100 Subject: [PATCH 179/194] silenced unshield --- apps/launcher/unshieldthread.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp index ea293b29d..6ddad7a21 100644 --- a/apps/launcher/unshieldthread.cpp +++ b/apps/launcher/unshieldthread.cpp @@ -486,6 +486,7 @@ void UnshieldThread::run() UnshieldThread::UnshieldThread() { + unshield_set_log_level(0); mMorrowindDone = false; mTribunalDone = false; mBloodmoonDone = false; From 17bf7e59a4aa5db34b6b69099f168574a775f07f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 09:15:26 +0200 Subject: [PATCH 180/194] added proper startup dialogue (still missing icons) --- apps/opencs/editor.cpp | 5 +- apps/opencs/view/doc/startup.cpp | 102 ++++++++++++++++++++++++++++--- apps/opencs/view/doc/startup.hpp | 24 +++++++- 3 files changed, 120 insertions(+), 11 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 3f6944a8c..1db5e0833 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -15,7 +15,8 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); - connect (&mStartup, SIGNAL (createDocument()), this, SLOT (createDocument ())); + connect (&mStartup, SIGNAL (createGame()), this, SLOT (createDocument ())); /// \todo split + connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createDocument ())); connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ())); connect (&mFileDialog, SIGNAL(openFiles()), this, SLOT(openFiles())); @@ -69,7 +70,7 @@ void CS::Editor::setupDataFiles() //load the settings into the userSettings instance. const QString settingFileName = "opencs.cfg"; CSMSettings::UserSettings::instance().loadSettings(settingFileName); - + } void CS::Editor::createDocument() diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 6c1e74058..50dc4ed33 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -3,21 +3,107 @@ #include #include -#include +#include #include #include +#include +#include +#include +#include + +QPushButton *CSVDoc::StartupDialogue::addButton (const QString& label, const QIcon& icon) +{ + int column = mColumn++; + + QPushButton *button = new QPushButton (this); + + button->setIcon (QIcon (icon)); + + button->setSizePolicy (QSizePolicy (QSizePolicy::Preferred, QSizePolicy::Preferred)); + + mLayout->addWidget (button, 0, column); + + mLayout->addWidget (new QLabel (label, this), 1, column, Qt::AlignCenter); + + int width = mLayout->itemAtPosition (1, column)->widget()->sizeHint().width(); -CSVDoc::StartupDialogue::StartupDialogue() + if (width>mWidth) + mWidth = width; + + return button; +} + + +QWidget *CSVDoc::StartupDialogue::createButtons() { - QHBoxLayout *layout = new QHBoxLayout (this); + QWidget *widget = new QWidget (this); + + mLayout = new QGridLayout (widget); - QPushButton *createDocument = new QPushButton ("new", this); - connect (createDocument, SIGNAL (clicked()), this, SIGNAL (createDocument())); - layout->addWidget (createDocument); + /// \todo add icons + QPushButton *createGame = addButton ("Create A New Game", QIcon ("")); + connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); - QPushButton *loadDocument = new QPushButton ("load", this); + QPushButton *createAddon = addButton ("Create A New Addon", QIcon ("")); + connect (createAddon, SIGNAL (clicked()), this, SIGNAL (createAddon())); + + QPushButton *loadDocument = addButton ("Edit A Content File", QIcon ("")); connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); - layout->addWidget (loadDocument); + + for (int i=0; i<3; ++i) + mLayout->setColumnMinimumWidth (i, mWidth); + + mLayout->setRowMinimumHeight (0, mWidth); + + mLayout->setSizeConstraint (QLayout::SetMinimumSize); + mLayout->setHorizontalSpacing (32); + + mLayout->setContentsMargins (16, 16, 16, 8); + + loadDocument->setIconSize (QSize (mWidth, mWidth)); + createGame->setIconSize (QSize (mWidth, mWidth)); + createAddon->setIconSize (QSize (mWidth, mWidth)); + + widget->setLayout (mLayout); + + return widget; +} + +#include +QWidget *CSVDoc::StartupDialogue::createTools() +{ + QWidget *widget = new QWidget (this); + + QHBoxLayout *layout = new QHBoxLayout (widget); + layout->setDirection (QBoxLayout::RightToLeft); + layout->setContentsMargins (4, 4, 4, 4); + + QPushButton *config = new QPushButton (widget); + + config->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + config->setIcon (style()->standardIcon (QStyle::SP_FileDialogStart)); /// \todo replace icon + + layout->addWidget (config); + + layout->addWidget (new QWidget, 1); // dummy icon; stops buttons from taking all the space + + widget->setLayout (layout); + + connect (config, SIGNAL (clicked()), this, SIGNAL (editConfig())); + + return widget; +} + +CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (0) +{ + setWindowTitle ("Open CS"); + + QVBoxLayout *layout = new QVBoxLayout (this); + + layout->setContentsMargins (0, 0, 0, 0); + + layout->addWidget (createButtons()); + layout->addWidget (createTools()); setLayout (layout); diff --git a/apps/opencs/view/doc/startup.hpp b/apps/opencs/view/doc/startup.hpp index f24d2a64b..f059a44e5 100644 --- a/apps/opencs/view/doc/startup.hpp +++ b/apps/opencs/view/doc/startup.hpp @@ -3,21 +3,43 @@ #include +class QGridLayout; +class QString; +class QPushButton; +class QWidget; +class QIcon; + namespace CSVDoc { class StartupDialogue : public QWidget { Q_OBJECT + private: + + int mWidth; + int mColumn; + QGridLayout *mLayout; + + QPushButton *addButton (const QString& label, const QIcon& icon); + + QWidget *createButtons(); + + QWidget *createTools(); + public: StartupDialogue(); signals: - void createDocument(); + void createGame(); + + void createAddon(); void loadDocument(); + + void editConfig(); }; } From 0e81fb32dd0ae7996ac698ba2d976398767811ed Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 09:26:43 +0200 Subject: [PATCH 181/194] hooked up edit config signal from startup window --- apps/opencs/editor.cpp | 10 ++++++++++ apps/opencs/editor.hpp | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 1db5e0833..c08abfc24 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -18,6 +18,7 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) connect (&mStartup, SIGNAL (createGame()), this, SLOT (createDocument ())); /// \todo split connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createDocument ())); connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ())); + connect (&mStartup, SIGNAL (editConfig()), this, SLOT (showSettings ())); connect (&mFileDialog, SIGNAL(openFiles()), this, SLOT(openFiles())); connect (&mFileDialog, SIGNAL(createNewFile()), this, SLOT(createNewFile())); @@ -127,6 +128,15 @@ void CS::Editor::showStartup() mStartup.activateWindow(); } +void CS::Editor::showSettings() +{ + if (mSettings.isHidden()) + mSettings.show(); + + mSettings.raise(); + mSettings.activateWindow(); +} + bool CS::Editor::makeIPCServer() { mServer = new QLocalServer(this); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 80336d66f..e32d7d8e5 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -9,12 +9,15 @@ #ifndef Q_MOC_RUN #include #endif + +#include "model/settings/usersettings.hpp" #include "model/doc/documentmanager.hpp" #include "view/doc/viewmanager.hpp" #include "view/doc/startup.hpp" #include "view/doc/filedialog.hpp" -#include "model/settings/usersettings.hpp" + +#include "view/settings/usersettingsdialog.hpp" namespace CS { @@ -26,6 +29,7 @@ namespace CS CSMDoc::DocumentManager mDocumentManager; CSVDoc::ViewManager mViewManager; CSVDoc::StartupDialogue mStartup; + CSVSettings::UserSettingsDialog mSettings; FileDialog mFileDialog; Files::ConfigurationManager mCfgMgr; @@ -55,6 +59,8 @@ namespace CS void showStartup(); + void showSettings(); + private: QString mIpcServerName; From efdf2961f0dd6ef25dcd49e02fa8e9c7e6a6cd35 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 09:28:43 +0200 Subject: [PATCH 182/194] open user settings dialogue centered --- apps/opencs/view/settings/usersettingsdialog.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/opencs/view/settings/usersettingsdialog.cpp b/apps/opencs/view/settings/usersettingsdialog.cpp index 21311c2da..e73e24dcb 100644 --- a/apps/opencs/view/settings/usersettingsdialog.cpp +++ b/apps/opencs/view/settings/usersettingsdialog.cpp @@ -1,5 +1,7 @@ #include "usersettingsdialog.hpp" +#include + #include #include #include @@ -9,14 +11,14 @@ #include #include #include - #include +#include +#include + +#include "../../model/settings/support.hpp" #include "datadisplayformatpage.hpp" #include "windowpage.hpp" - -#include "../../model/settings/support.hpp" -#include #include "settingwidget.hpp" CSVSettings::UserSettingsDialog::UserSettingsDialog(QMainWindow *parent) : @@ -29,7 +31,11 @@ CSVSettings::UserSettingsDialog::UserSettingsDialog(QMainWindow *parent) : connect (mListWidget, SIGNAL (currentItemChanged(QListWidgetItem*, QListWidgetItem*)), this, - SLOT (slotChangePage (QListWidgetItem*, QListWidgetItem*))); + SLOT (slotChangePage (QListWidgetItem*, QListWidgetItem*))); + + QRect scr = QApplication::desktop()->screenGeometry(); + QRect rect = geometry(); + move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } CSVSettings::UserSettingsDialog::~UserSettingsDialog() From cae5eb424d8d2a4ac31ae0e588fd29a2b47c7ad7 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 09:33:45 +0200 Subject: [PATCH 183/194] cleaned up opening the settings dialogue from a view --- apps/opencs/editor.cpp | 1 + apps/opencs/view/doc/view.cpp | 9 +-------- apps/opencs/view/doc/view.hpp | 4 ++-- apps/opencs/view/doc/viewmanager.cpp | 1 + apps/opencs/view/doc/viewmanager.hpp | 2 ++ 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index c08abfc24..e9a4a4089 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -14,6 +14,7 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); + connect (&mViewManager, SIGNAL (editSettingsRequest()), this, SLOT (showSettings ())); connect (&mStartup, SIGNAL (createGame()), this, SLOT (createDocument ())); /// \todo split connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createDocument ())); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 6801ea20d..ab175872f 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -67,7 +67,7 @@ void CSVDoc::View::setupEditMenu() edit->addAction (mRedo); QAction *userSettings = new QAction (tr ("&Preferences"), this); - connect (userSettings, SIGNAL (triggered()), this, SLOT (showUserSettings())); + connect (userSettings, SIGNAL (triggered()), this, SIGNAL (editSettingsRequest())); edit->addAction (userSettings); } @@ -415,13 +415,6 @@ void CSVDoc::View::exit() emit exitApplicationRequest (this); } -void CSVDoc::View::showUserSettings() -{ - CSVSettings::UserSettingsDialog *settingsDialog = new CSVSettings::UserSettingsDialog(this); - - settingsDialog->show(); -} - void CSVDoc::View::resizeViewWidth (int width) { if (width >= 0) diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 56c0b3edd..41e26a6ba 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -112,6 +112,8 @@ namespace CSVDoc void exitApplicationRequest (CSVDoc::View *view); + void editSettingsRequest(); + public slots: void addSubView (const CSMWorld::UniversalId& id); @@ -160,8 +162,6 @@ namespace CSVDoc void addFiltersSubView(); - void showUserSettings(); - void toggleShowStatusBar (bool show); }; } diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 7c7c0f28b..984c81937 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -114,6 +114,7 @@ CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) connect (view, SIGNAL (newDocumentRequest ()), this, SIGNAL (newDocumentRequest())); connect (view, SIGNAL (loadDocumentRequest ()), this, SIGNAL (loadDocumentRequest())); + connect (view, SIGNAL (editSettingsRequest()), this, SIGNAL (editSettingsRequest())); updateIndices(); diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 1f4dcd51b..1f4efbd2e 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -61,6 +61,8 @@ namespace CSVDoc void closeMessageBox(); + void editSettingsRequest(); + public slots: void exitApplication (CSVDoc::View *view); From f7940d7d1ab9aec017ae724e6c385bacd4d4fc7e Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 11:24:06 +0200 Subject: [PATCH 184/194] focus load document button by default in startup window --- apps/opencs/view/doc/startup.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 50dc4ed33..010fbaf16 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -13,7 +13,7 @@ QPushButton *CSVDoc::StartupDialogue::addButton (const QString& label, const QIcon& icon) { - int column = mColumn++; + int column = mColumn--; QPushButton *button = new QPushButton (this); @@ -41,14 +41,14 @@ QWidget *CSVDoc::StartupDialogue::createButtons() mLayout = new QGridLayout (widget); /// \todo add icons - QPushButton *createGame = addButton ("Create A New Game", QIcon ("")); - connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); + QPushButton *loadDocument = addButton ("Edit A Content File", QIcon ("")); + connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); QPushButton *createAddon = addButton ("Create A New Addon", QIcon ("")); connect (createAddon, SIGNAL (clicked()), this, SIGNAL (createAddon())); - QPushButton *loadDocument = addButton ("Edit A Content File", QIcon ("")); - connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); + QPushButton *createGame = addButton ("Create A New Game", QIcon ("")); + connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); for (int i=0; i<3; ++i) mLayout->setColumnMinimumWidth (i, mWidth); @@ -94,7 +94,7 @@ QWidget *CSVDoc::StartupDialogue::createTools() return widget; } -CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (0) +CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (2) { setWindowTitle ("Open CS"); From ecedb601696b7664666c6c80b75879e018bf4990 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 12:06:28 +0200 Subject: [PATCH 185/194] splitting new game and new addon functions (new game currently not working) --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/editor.cpp | 20 ++++++++++++++++---- apps/opencs/editor.hpp | 5 ++++- apps/opencs/view/doc/newgame.cpp | 14 ++++++++++++++ apps/opencs/view/doc/newgame.hpp | 18 ++++++++++++++++++ apps/opencs/view/doc/view.cpp | 10 +++++++--- apps/opencs/view/doc/view.hpp | 4 +++- apps/opencs/view/doc/viewmanager.cpp | 4 ++-- apps/opencs/view/doc/viewmanager.hpp | 4 +++- 9 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 apps/opencs/view/doc/newgame.cpp create mode 100644 apps/opencs/view/doc/newgame.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index fe0415ac0..8b9e31e3a 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -43,7 +43,7 @@ opencs_units_noqt (model/tools opencs_units (view/doc - viewmanager view operations operation subview startup filedialog + viewmanager view operations operation subview startup filedialog newgame ) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index e9a4a4089..afbe233e4 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -12,12 +12,13 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) { mIpcServerName = "org.openmw.OpenCS"; - connect (&mViewManager, SIGNAL (newDocumentRequest ()), this, SLOT (createDocument ())); + connect (&mViewManager, SIGNAL (newGameRequest ()), this, SLOT (createGame ())); + connect (&mViewManager, SIGNAL (newAddonRequest ()), this, SLOT (createAddon ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); connect (&mViewManager, SIGNAL (editSettingsRequest()), this, SLOT (showSettings ())); - connect (&mStartup, SIGNAL (createGame()), this, SLOT (createDocument ())); /// \todo split - connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createDocument ())); + connect (&mStartup, SIGNAL (createGame()), this, SLOT (createGame ())); + connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createAddon ())); connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ())); connect (&mStartup, SIGNAL (editConfig()), this, SLOT (showSettings ())); @@ -75,7 +76,18 @@ void CS::Editor::setupDataFiles() } -void CS::Editor::createDocument() +void CS::Editor::createGame() +{ + mStartup.hide(); + + if (mNewGame.isHidden()) + mNewGame.show(); + + mNewGame.raise(); + mNewGame.activateWindow(); +} + +void CS::Editor::createAddon() { mStartup.hide(); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index e32d7d8e5..7d86358b3 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -16,6 +16,7 @@ #include "view/doc/viewmanager.hpp" #include "view/doc/startup.hpp" #include "view/doc/filedialog.hpp" +#include "view/doc/newgame.hpp" #include "view/settings/usersettingsdialog.hpp" @@ -29,6 +30,7 @@ namespace CS CSMDoc::DocumentManager mDocumentManager; CSVDoc::ViewManager mViewManager; CSVDoc::StartupDialogue mStartup; + CSVDoc::NewGameDialogue mNewGame; CSVSettings::UserSettingsDialog mSettings; FileDialog mFileDialog; @@ -51,7 +53,8 @@ namespace CS private slots: - void createDocument(); + void createGame(); + void createAddon(); void loadDocument(); void openFiles(); diff --git a/apps/opencs/view/doc/newgame.cpp b/apps/opencs/view/doc/newgame.cpp new file mode 100644 index 000000000..eea0823a7 --- /dev/null +++ b/apps/opencs/view/doc/newgame.cpp @@ -0,0 +1,14 @@ + +#include "newgame.hpp" + +#include +#include + +CSVDoc::NewGameDialogue::NewGameDialogue() +{ + setWindowTitle ("Create New Game"); + + QRect scr = QApplication::desktop()->screenGeometry(); + QRect rect = geometry(); + move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); +} \ No newline at end of file diff --git a/apps/opencs/view/doc/newgame.hpp b/apps/opencs/view/doc/newgame.hpp new file mode 100644 index 000000000..7c9e92a76 --- /dev/null +++ b/apps/opencs/view/doc/newgame.hpp @@ -0,0 +1,18 @@ +#ifndef CSV_DOC_NEWGAME_H +#define CSV_DOC_NEWGAME_H + +#include + +namespace CSVDoc +{ + class NewGameDialogue : public QWidget + { + Q_OBJECT + + public: + + NewGameDialogue(); + }; +} + +#endif diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index ab175872f..b2e848aaa 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -27,9 +27,13 @@ void CSVDoc::View::setupFileMenu() { QMenu *file = menuBar()->addMenu (tr ("&File")); - QAction *new_ = new QAction (tr ("New"), this); - connect (new_, SIGNAL (triggered()), this, SIGNAL (newDocumentRequest())); - file->addAction (new_); + QAction *newGame = new QAction (tr ("New Game"), this); + connect (newGame, SIGNAL (triggered()), this, SIGNAL (newGameRequest())); + file->addAction (newGame); + + QAction *newAddon = new QAction (tr ("New Addon"), this); + connect (newAddon, SIGNAL (triggered()), this, SIGNAL (newAddonRequest())); + file->addAction (newAddon); QAction *open = new QAction (tr ("&Open"), this); connect (open, SIGNAL (triggered()), this, SIGNAL (loadDocumentRequest())); diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 41e26a6ba..29a1d52f7 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -106,7 +106,9 @@ namespace CSVDoc signals: - void newDocumentRequest(); + void newGameRequest(); + + void newAddonRequest(); void loadDocumentRequest(); diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 984c81937..a5fe6d356 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -107,12 +107,12 @@ CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) View *view = new View (*this, document, countViews (document)+1); - mViews.push_back (view); view->show(); - connect (view, SIGNAL (newDocumentRequest ()), this, SIGNAL (newDocumentRequest())); + connect (view, SIGNAL (newGameRequest ()), this, SIGNAL (newGameRequest())); + connect (view, SIGNAL (newAddonRequest ()), this, SIGNAL (newAddonRequest())); connect (view, SIGNAL (loadDocumentRequest ()), this, SIGNAL (loadDocumentRequest())); connect (view, SIGNAL (editSettingsRequest()), this, SIGNAL (editSettingsRequest())); diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 1f4efbd2e..01f495186 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -55,7 +55,9 @@ namespace CSVDoc signals: - void newDocumentRequest(); + void newGameRequest(); + + void newAddonRequest(); void loadDocumentRequest(); From 294bf6c1941f78720744b075f8c0e7a5343eb23a Mon Sep 17 00:00:00 2001 From: Marek Kochanowicz Date: Sun, 8 Sep 2013 12:09:36 +0200 Subject: [PATCH 186/194] Icons for startup window (check raster/startup). --- files/opencs/raster/startup/big/configure.png | Bin 0 -> 17972 bytes .../opencs/raster/startup/big/create-addon.png | Bin 0 -> 37360 bytes .../opencs/raster/startup/big/edit-content.png | Bin 0 -> 70441 bytes files/opencs/raster/startup/big/new-game.png | Bin 0 -> 66006 bytes .../opencs/raster/startup/small/configure.png | Bin 0 -> 1450 bytes .../raster/startup/small/create-addon.png | Bin 0 -> 1714 bytes .../raster/startup/small/edit-content.png | Bin 0 -> 2471 bytes files/opencs/raster/startup/small/new-game.png | Bin 0 -> 2122 bytes files/opencs/scalable/startup/configure.svgz | Bin 0 -> 5808 bytes .../opencs/scalable/startup/create-addon.svgz | Bin 0 -> 116154 bytes .../opencs/scalable/startup/edit-content.svgz | Bin 0 -> 120969 bytes files/opencs/scalable/startup/new-game.svgz | Bin 0 -> 113710 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 files/opencs/raster/startup/big/configure.png create mode 100644 files/opencs/raster/startup/big/create-addon.png create mode 100644 files/opencs/raster/startup/big/edit-content.png create mode 100644 files/opencs/raster/startup/big/new-game.png create mode 100644 files/opencs/raster/startup/small/configure.png create mode 100644 files/opencs/raster/startup/small/create-addon.png create mode 100644 files/opencs/raster/startup/small/edit-content.png create mode 100644 files/opencs/raster/startup/small/new-game.png create mode 100644 files/opencs/scalable/startup/configure.svgz create mode 100644 files/opencs/scalable/startup/create-addon.svgz create mode 100644 files/opencs/scalable/startup/edit-content.svgz create mode 100644 files/opencs/scalable/startup/new-game.svgz diff --git a/files/opencs/raster/startup/big/configure.png b/files/opencs/raster/startup/big/configure.png new file mode 100644 index 0000000000000000000000000000000000000000..f0be888a15532311418c82714d7cb56dd5620a09 GIT binary patch literal 17972 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 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/startup/big/create-addon.png b/files/opencs/raster/startup/big/create-addon.png new file mode 100644 index 0000000000000000000000000000000000000000..fa059264ef5fe5e8f64d6804e5cdf0603b039838 GIT binary patch literal 37360 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 -JC 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 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/startup/big/edit-content.png b/files/opencs/raster/startup/big/edit-content.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb2602e5a55a8992ebb106c73d66c8762936f08 GIT binary patch literal 70441 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 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/startup/big/new-game.png b/files/opencs/raster/startup/big/new-game.png new file mode 100644 index 0000000000000000000000000000000000000000..5cec444178e6429377e80798d157ecb212939b6b GIT binary patch literal 66006 zcmb?>Wm8;Tur=;3!TrGocMSvyF2OyxJA=DBg9LYXcXx&aC%8Li%~@|0GT%{oEvS{jTk*=4j#SVeDcK<>BGMYVBYPG&OcIXLWS3 z%(@UJhJvDml9d!w_sl-)(W|am(SSUV+};>0>b|%u<)~@YsVQdi!-4 zg5g-LPtNJ4PtBddVYcbMvA<83ogjLcep#iykBlC~>au0AZL#f;5E%RM82^FCwKrCK zHny2@xOv9sWUl3I?wD%;AIQ|z=u!XwD~9pCpzE)s^kCnQD~lX|TpOmDhrBp%Ydt=S za=!IKa+#EcuR}j}`rP-oK6X7i{z>3ox6S(_dR|U$%?Iq^(jP?h{iF5c+Ikt{?jT0! zY-}rhJK>u4^e29Que^gVcSXCX22*9;55ssmG;Cs?+D?>6*KANW=AXv5B#CXrJBK9rNQ# zp8jL{?c=GhH*d2z{$u700M$LDAh|z%%l`sojWzV!MV_99rHF`!W$V5Ye_F82z5ck8 z4wyF#*uno;kE=mM68l5DQj^9TX1pEt(5tnx^vL&-)^xMG^%&S$lN^zAv?ccDOCV)@kU zeb6g^JrKRs%EtV)cYiNrK0^QW`$7J_BQL}!@5A>=FW`abBaH3!o9F(;V$Dm+*DE&O zYoonO{{wffVa;6!S80P(=T48wG#4j4bDF7|_(jEl=8IQj*KEagL9%dLMW?LAE;lp> z8Vg&VHKS?`w<4QUtf5lJK|qEKYnUPd|It@g6uc1qezXMm1{^GQ=XBy-(p|XW89ymC zI=Bcpu}1dtdXY22CW!ABq+DBiG9~y)lQomgDaDFhFcsf0!kO9bSyjB zSTmE5ivQJnDDI9pu~o1Yi5lM7|LNmKhyV*YlE2+oA03WbD$b2aX5WvxTDTl?@Z#g! zw+prbKDBC0&)lj_8Tw@)T3W`gq(@_iz+%g(sGswB487NV9^2pLC86(zZ@m_#pTw;1 z`DNPk(d&Ja?JX(7a4*VI^yJ9!HV|5uCO^p@l@T;V1k#r5o6Qw z`Dm#iG0v;4v09vieaQE2t4T$G3?*XLIWu;0C$lv^ou4z8ko0TfV_BvYyfZxZ7esD` zuyZsu&Tf9)*xB!|HsAYx&PLG-vNKvSDge$NxDYvZLA6cqTo+XtCN_75Y7=O4&jG9- zsi*N5^>2aS*-6>Hrq2ifUw=~}D)tKuC=vl#Mo2B!Sjrw8_Y|FfIw_??CbChfU-{

lV9oGR~x90$;SEBoY_<+|&wVRc^kEgl{_>Z;t0O8juHRTsu!}s8=@U-0= zL^Y!W7VDKC+5}B4#96%IZgayL zB*hd>q>P>Jo3atknz60O*>=dr^=qoC<((XQ0ck-|de3-jQc{X0?6$KhPylu|yP6E4Y@ACkZD~-Bq-I1! zvjxA#1j;YPv(n)6wlinpUJ=hYAX6!F6LQmVHqKB&x+*l>Wf&bw7B~p=n?<|mUk~|? zA1LNpSF>Wco=?nkUeh83nqo+J%=k$`hB@^j9b#LO;B!Bv%Pnw}5g`yz4%qzD_g441 z?)avx?1!5Iv!xFG|A6tvCi%ZmN(k6W>pSapd71FLg5}E7d-uQnqX!6BA3N8()FSsu zo}A3u)pFAV&2r{pQ zYBClS(I1(lsj8Q9HSMV8u0jfiLo|Sc^uy;H>*Rb3<{skaQVJ8m4>vtB__U{Et04m(NL^l%fWUEyA^2Kx;PS-z=MWl4A16}HY6g%7o@V=*!Se5ocmjp zWDI=Qq#yOf=8VqC+fgTKxu_Py)4~zb&@?FirNB(o68wdhWCpIgH9aVUqmq-OVN02N zufH60VKx-x;TfxL5pI{Ahe@hyY^6zBL+S_DY$Qtr8C*#qDjTk}kAw+NM8Drtzzq#y zunRV-%)UU4AkXga4SWz^^SX$QmZMAwF2~`Rpsd)*F>N^!Xul^cIPdUy&Vmz&+%mFB}mQ31AW=H4!3Kyt)9sRh7 zr&2QC%-t!I>|d_h9Z_)s%N5vDi^W!aV90=)}-=KQu4HR~Z(=R!$83 z6P*HVANO8d5KQ}O&aCjOk!gk(9bVa|-D07}$rFVg=1l3&;SnDqcOc?@|HXhGF=r8K zvmb2cpGveYXwldS0W}6smg3|U<)~!No(MD0ZrC>boC2{FMpnPl&UXH&v~&jQZFWMn zEBQT8ylMAqGR!UHg))KBL>U!%r>tlibeUxvAU=ztl9zGD$???jY z<*G+8l>+5w?upO`8Ai{+_RNF{>ld}zgy@cr^yKP7ix!O6P6~}T$ou=pCB*S#l48ed z&Ulx9Bd;+7*M}m?AQXm|U~x8a_}4_FeatQS_zYjr{gGgmd_=EIJpMQ&`x5tsaV|8_ zLmZ3^0ttwxo}Q|X=6=F?<-+n?dW^H@n_!Q>eUDJ4i-(7t5JnnLgRb$(lD?xBxBB+_ zkNSy;-#u%Pv(XHtP%6d_hN%!WW6h#;oSq@s9tsY)#W>4!RKL?B;&{H_m9erJz)6_G zhjfkYN%qp>iAvIHy=OOhTw$FQ>oGeIE)204YLn_htm4`}eWSb-YfVi*iE?~&jOD2? ztWXhtT@J~V?!d@39qZL^Bs6PpzrjN)c%|yIzC79^C)kI}w&F45b`Sz_P0jKHlsgDw1Q;79N6 z`)FtF3%Qz5($NpD)}n%EHm7yReG(l5$g?RQ)X^XAhs(dusmmVw0y|BAPlkWOAZFoD z3H&hrjeGHm$F0U10x}Wgz{KSDt2a+iQQT`6vQ%XZ-A0;5WkVv(A)T3HaWomPr{G_k zDo0`UR7YU0E?;Pcj!v@b;q**zqhaN(j*st<9X9l*gwJB9^OgNq%bq&ur5TJ%n(&K* z)^sXr5%sZ?@(f1#DmYa3ta^LjC{RhNkJS5QMx*O zu2Z`r<(0|u;ZK|4;r>1`6N-)A;d$;xD~!G$!3y0nCbBbo%wE0i?+&M2+zYesBrw{8 zDdKqMLY5%?{j&oht4r$;ns(i$N))=8bQ<@>WqwUFVHg?s$+1Wfaw>68%itK%i~jN? zimJ!0z*4?OBbJ{V@xW}9Dqgk+T8Gg$wN!H78mDh@$xP&_8qS-0A$373$W(&5m;K3f zNMFK|s0fOXgWj@#V+6y}tA@m?G+{x7ETYzM6*4v^xK}eU@VAU_7nDj8U^|~h@948w z2E%?|HaE?SbKBx9jg8!fXD@zd_Qq%I*?47_!8AvhPLi z@8@HI^4+JQc=Gs!9b4dnUICgw7ck=OHn@y>zN;7Fq-2I;o|zw890CiP>^&@P2|N|z z#k_Eaq!8$|K|3O6aS1TBK+`T*0}M-28DO9biX~Sux#OfKu2Q2);W(1SDZ-kX5_ypZ zBoapcU?T;yfn-7)p)$m{_KszkC~3K3!x+tp*(8$@OlJoDS(C%mxh6Bf0u~&>9^BCL zvB{{cNRg&va@WCnF4!%)w|iz!eM4T#PJ_Gzntt*WU-_#NBDof!kwA#1g>gY{1VpM9 znYOY&#xYMUacN}S9JJ6Do>gshn{c@fQK8s8vyp$sWtO$+&7NF{>^Aa95t1Mwe^A8&j%!Pl*=e6&!$|B5Ky z;#XVWrnk-vK3ZBy=!C&`80UHzFR(BPP?w}lmjVu$y%*t(AQorBWs9S)XAd+ruqstm zL*EvVX0aEzCc~Ebv&A?@Ol4RJbssJZJ*$qd{ImJBdF(pFt(DH2aEePOmEcsME%E3G zQIOEq6iCp9*~=su6=YYYTMh|Wl2v7N7-0e)OMHAUR=Qr7xh@Y6|DZs~wi#9kdm3)U zNoc6^>CzdL@(XrJH&N+QbZ5Zd1|#jw7JPi20>uG8P)wvKDBA}8l@zF}Gb3Zz*>b`Q z)|2DM}D#OL(Q$b-{io$Z>aRQo~*VY4(S2}}tsR<~`C)m3mZ z36F`DISCta_`U)_=gI9s4J}X|>fKOBJBv*8(?6Nx+giTDHz;zPRBdq_lcL=HyIL#+%Wq%I=*eyY@CmoUM&r(YaXQ20@c zW8BP|5*7Mg<+`DH)|G6K&oJ5@H)86&@E3g*ar>>=Y{ei%Xn< ze}jznxRC=p3QsxHJ^c?K^2$!#$2wgKbgJq<2#&SLbQ2{QGC1+OVJ~5+Jfo3Ae_L$) z?SN@@B0WTtf2j>l7l+c-x(?fptxf=1XqvJORw6eK>=+C=WM`l?F5amcen(O~{}x&0 zx=|b+`fU#mm}@W=T3Zwc+Dvx4*RI9eH;FbW=LZ!+xEZSyr@iN}b4H5mRG}7kHp3}$GN5z7Y>mRWFT+_n0xb|!Bzx5`cpu?#KxJ}H%k7RZUe_H9 z)+0F=%9Q%TPolc|!=uz#saXj*juL};iM^08Glh!jXRK_Ty=KzDrT>M->L$=-W;^8D zShD)#5L@?{wCEKa$Gr^iYp1E7>%^E)Fk89+vzHtR!*|ohw*4HvDSa?gRG!|_WS{(E<3Rxpr?O^7LiB8c=WP|# zGvEQEt5`bS2wTq^Cu5n8j?L_HR0eAqc zG`vo%6^l{t63KCVuS^`x=F)jenv13R31t^GE`Ead zm#bL&0+Kvg7Quhrw$$w~UJWV}p2F?M&oZCe+>Jy2#8#8`pZ6r$R%&%_YKbjs-j0F%pNX3cj?h20ZBtg3h>d2kZoL$pl1UL2ch7)fx+ zh}os_*j>8+EuvX?i`{1#dfN~4*1K*rykcH=B^FEg^ym*l1NJKAuqq`%b@It8s-qp|*!&&IOlwHVTb;5=aMrEj4kwa7I$uN;t=3 z-z1jtSJG@bmH|9*jF=dMiR&y!j41>1j;T z0G-;&6~e6okH@HCLDl#_7nN+hAKe=Q63 zY6hlXTI43NUAM-e2mdr6(3$nKyoYgvS@BoFz!{Vp?XcXK!jh>N%oT=Rek+?r!0MjpAnJ`|2PD(hnf58~`nzNoZRZi)+6Y$q}Kxyo7 zeZo?G$P-rqNucRxOEtB8M4@^|aUe7dhcyZw@8=1Mk8(0kjwWIqb~_Ix0X1g@nlDgKMd@L;`pLfk(E2 zwcx`2IpF%;yr92V10mF?uhX~6ayWASeG})&e)4c*Xa8mV16OrTgCX}Z(ps8PCd4GF z)=Hw(~MSm~cgfNq_@sxvNhq{mR}3bCOE*>5npo=wpZ|{}64= zJ&R?VN5(_%hjZ|hXAUwD9L<@wDm1+Uc#_;PT1blFoGw{x)Krtw(fr;RjShEt91I!o z-@U3g-{8}ah4Ua(yo{7x#u#&$a#cJ)DMz02`A;&~lN!t%bm$}@Pnk~r^AAkXqQSl0 z#6)4Uy6Z-SAYPMV%X^=uV93sg7ZaA zWf*>y4$EZQK3FU(fMa zNb5*+>G$<1m9aPnftL`5@$44wUs{1GG*enSYNd(+8PF*xt?81A6Hkf{T-3r=CJ+#e3ZP#x3Q%Y{N{%p^Rr95f2EBl z!Vrh_#cB_{EY!cOCrK@d;l*PHdjzv`L8qdgNgtsF8$O$4Y{zacEXj;#-Y97c#kUP_ z>#o~;U*m_tb;#gNW~33=`9X}?Ma5AX`~clDXqneDUKem9@V<)Kok#CT8a(*mcg69u zr_Y=a7y5{_y3q9geu0r9=W?QqI^2lwM2yvTh{e2B+?T!c8l%2%k% z{A+O%QUh1~GN)i!4RY}+2P1&oB|s;-1dakQ8eaQ!a;u#y;T4zXrS;M3#9-5G0Iq1U zz;5}jT`?$K1!^+3nCsBJO`G8U58R>jJ)FUe3%qjF6>gl~-T_KLnN#n3;a6-{^btVf zA!!HqoEET5@Xe6BLw%0GDpmU!-Thl>;%Sl>f8}@7!$qN@A5=RF0j!*YFFS;^l(=aZ>r~^s)Zj!4#@WejF&?cg@tTb6UglxJzd4 zA+&ZRaO>q?jIj4@`nC@!c(z>-31040cyl4_pk@=f<~`m+J|SlCxR^9Uolz7Tub5C3 za3GDH-L`~;x%7=v9VbPv5jJHkD_zdsO^N7P1ScEfDmp7!pa;&@K)00oC%y;pvQ_1u zmfDg6Cr8NxMiB*p)@JCr14Ov`+K>GTn*xII|03GmtAwu21!ek+p? z*K1=#aXXo0lZB;=BsPgwz~x;sZ~&*Syva#segWK6qiE)cM_-S zy$!lEj(-4wg?eA!_`HrUa;N>D$ca7&H*o|a_x60pou%%a!?UinoH&@8q+|v|&_gs= zdVLS(tCqQD>u^fcq@IyKu?#QA?byvInZol;;3_g<{jijVodp^Fg4{a@iWVVc-rqvS z;yTnvJ?5csiVNQf_N7S1{?7{#r3pyk=}->$n%*fj&P0Xm$qIU-w_!z<#w1v!Il+al%6MhA6@6Bq5p$ws%*}1e zd4|DKmdL3(gavIqy1Qw{IAng*$Js4?Ga|HFF))|?&G;ECkQm43axBpJ@#Vb6+gYIw0KdEBg*2Ec04m6sn{fie{8m%vP z2*<5ao^^TihDt${>Zr2{={t21BIXIr6wV4FMr{sMLJj+RPsBq8JT6>NVap;OmkE|G zeRi4nlFu_Q`*c{6Er$vdA~aib%_vj5{<3n$+_{XClYi&Iz>2ov&#EgVB*c`1LZ$!WqNx*~ytM6IY)?`ZvkB&U;m#F$2)Xz-kK;GQ_Y!u*C=N&61oaeKX zjM;sfV2^VTEQpdKF`jGtXYil5&N46<6|2XTZ*sqwbYxY~PW)iS&?(EEXv)I8$L4yI z1^Aw#!mpHJP+Oygty3Bsrbt4l3eNZoFRjhlP&l6vXBK*dJg`$9XuqcJKcZd^5UWkf zR;}$$ApCmTDpH1=yWU*v9HE;dKeL;Mlc?OCqm0%Gfc|Ov!f3dd+K3c$s2`aIL&0-X zrpf~;3^8D>me5FEfND6m8X??^_H>ezSU}DlZXzhU1DTA6>Q{7qxE6TQsDA$*%>fZ7 zfSvU^R2@gHKG{z(Ia4(jno9ZnK6#nPQSl~b%C+TJfbWPM)C zx26<+n0+5lT2EJMd^U)%@z!Lm$P8bjvgpIKDE_kvHl)9OS@e8gP;(${5q6oG^D4Ll z(MfUE=U8+jadc+3W^o{nmR1PeF@nXCD=V8PUM}TnS!$k7UV&5iT?v`rx$W~6ctUjc zr6OndunvoxQQNI#HU@wj$C!&OI;VbNfSaV41~#6mU+~q67Q@>s+!`BRLKEq9&$h_; zVj8DNF1*~3GXIk@)-CYbUvhVZt5ovbWo0+Ol;p|pnmU>Rdzxxu<8-F3TFl0O?WY*E z^X{rx@k_(=bvpK-6(<2$^@Xt7Joc@QQHS5$C?wMOqxI4KhRov92QzxrpjjF1a2q30 zOK=BP;og3Ikrn<_yN{srgYPbr2f{GQDT8aR=9o^EU29tB3%qpB@1{(-fbFkKo+pbO zQyOlD95iZwIN}1wqolJ2%SL_rM>2c^2{QA2pGpwdg-?hoSVSRVZX`#h`L?0%JO3q* zKu?1+hI)5g`Jkn4T58!!+{uQ8e4UDm6)NVfcfAR}DQ@+sn}Fu2D%H_qg5z^nQvXVX z>F39x=Ww~p>O3Ug2`*x_&UD6{6iWZl^FQq!fracTZ1ICuE3FAxI0&QxVT)2JsTUvzQPB(HuO9KExs`pf>9PjQC;sH>_M8xy zDmi#Qe6&@9?yBdZSrcw@5dO>oj>WCz!wY0TZ8kCpSO>%dDy+LvBpA)GF)#&JQ!J<4 z>=t93n_1UIH+D-f;L!ng^t7zwO(1MGVUmhuQ%9bDetK^E_A^N{IO&{arf;%ird3Eo zl7PD=#2`9;-@WCN{*5NGt^0js`>mPb@LFW3zH~b`S_67MEQhl>(@i#rN>G z?&?ToG^%cZ=3tl>b$PAC-w2kQ`s^@c&uw)7ROVVdMLZ=sjxy2%&RLScih$>UWBMPW zZKmM8b`b(Qy{RpTPW9;k$5e7)NOBh@3mVZ?iw(@IkaKhFg9*+q^?2Y8=tkwMs8wp7=Sa=4eX?1R8@l!(x2Yk&z~SSC16Xfko7xzd5(o68PthJ738D+v zMn~B_fiz-F-Q()SJ!}Z>sO1HODFLK7@o7rL0diT%AE&+2rcnrDUQL7!WxevXObr zhh%J~x~D+<=Af%l@sBbO7K;Nq?w=(}{+;L-1?-)g-0glbr+5*hLJ34Jos;{}82O?y zv27$LxAfip<;i8ZBt3|K73=ic)TA1Nccx}}OSH(jqI64_A@h&G!jM4eFpy2eNXUX$ zF~^+WhV&2XBi%3nS0Xn4ZWxBnP z%U(@H=WH4^tN#)dGfG3t$aBmaD)^ytsihhQ_3?PDG7&C?+Z+t`FhxHASqygFrgJFP zDz7=w*NBGgLp6K3S<+-mWSc(j;^+S5oL@|}|LahQ(|5W1KqcpN;}(z?`w@*?fZT3f z4na8jo6`W`a~A-12WGibm0JCXW?$5+#|pF)c$NIU(r#8l!f%;eT&}fZ(ga>Tm*Q6uT zcbIW`60^bsUA!HqnaXh2I82e)w$>UiNKacEUv26uE?aH~{|Md;)7YZi{ZQ?$I{(H& zo(7)>xyo_w4D_Q{_WB8q0&s~@ermpdNQ%;W3sp03<3|;1ss}_ zqh;I{I9W31!|U7sIfnpRRphf{R~ti`zWJwlhW<#}ZQfQ5_M%g+sH87ZP-=M-I4r@N z_5NL6QCL~5KhR?g5GF*mG~Kb|3^(ySsaM6_7w5#%(T*lxRt#(EKp1bmlX1$P%rM|x zJmsOKcVSJj{NXu0{eCvxZ6DMhVE`kX|2(MI>c&}iBG8!dHFayy6@1~UEF0E~Z9;G~ z!lCoV&~TbI%4YdEg{Y{IsGvTe zz0Fr&z2qmc>>A#95y}CMqea1n+ViO1CVD&()Tada2>bGW`SDKXbj4ox;%!HG%G{%7zIkqRZbUB7)n!VPg4Udu z8b<5h5uy=o4_yk7Q$Z2mP<&WfrX32lY^nU^h6$Ejn`fqOUwz2yW>?_yBVgJUQL?Ix zscD6YV;$tzWORz(Ycdo#@U4yT21v#TrV96_jSC3AGTQV8{7Vy;qO#eY3!SjMPK$ zv)ymZ$I-#c+YnwBQmNK{LafSKEHKB5A&u^BbC(~_>uqw?0@9-D5YHT^1%CGG5F0Rl zD#sq`-|6%duN+qOmVJvSsnk8xr~;SQh+<(vT)%2_y{lSx*g}bsYa=-Os@_e-g!QTkF4`{@X(2d+*2n?gJ9+R9SoP>N2wp_g5eOI~pSQ8_Mb# zZegLb?s9VYRo|*iZbk$`PI<9jM~-_kwuSw!nhPo^Q4PtsE17$nujoU2E5zo$Z1$_i zZe)M9@?qmtm43A+-v-*o{ZhTnGJ)I!JK|qiedWR!@iVo@yPFEDxt(X)T7DHIz`L7z z-MoG5=kc>DvHE1KNMJ&hR_<go0e>cGc>EDH#!S0%=ufe zRaZwVbJC~dGbK!>SAyxPwljWlPTbjbE~>;&OjUr!h~X-vgL!@u{+&oJxXnEh?lP9r zKUd_1LCo2+>w?Vgfs#@x@LDD4@Rz?B|JI<`KI}9Fq^!{I{!kmEHoxD^VVTlE*o>*m z1)k1IMB{hP><%w%D1|!9{M+0KfsRI6_g)I$Q_RO*hODBjW6CiO)~Xjw+h;!YuzebC zF8$%UXIpahV+&it($-ywleiWe2E2?F?%3x5)v8ve!yO;-J$&aa%sm$`{-;Fun~-dw z`DHbpKczC!T-JXQ#dHc;_z`d@p>H^UYwh?PH$7@3bmJ!Q|1hI$AWJM{P%(@7+Spaq z)oI&4Bw&d-U4LWp74pTLA44zoMHAXPP>Qdz^FridanrW$&hTUB_{0g2QOS;$_=`}C zw1v3MBo{S$H}i7fxSpCEM&ncrY(Q8xY_YKu%z-F`t+^Ql9L}!}53Dq2OHSn`{Voud z!uoHLRO1hde1ew$O`P;aK=lvz+tvNcB=$=f;cWIIad<1Z-ct z%5?u%JI3s4!PX8Z;FU5E_DfT{3Xf_GAQkFA5FnM-T7)Olv&f8(G)p~m1}265 zpOHZUISsH`>rsl_Pn33)0Y0I+=+v;2Drj%(0k$xoGT2K{S~eQSyz^fH*0R9^Z5eoO z!S~k-1dA*p)Ak|T0vI#4X`l0`jNJt87EjB6mQ&Gjs$1b+uf~{iA1GiWu!{^Gc zVKe24Z{ciVeTW`=COAij)LO#MC&h)^K}Z0K`0YPc(^kJL)zX0MScGTLrKMKD$`x}z zu~0yZ+f#qBK7O$w2zBeWUiZ-f;bMd0N!!lbfaINLA4?Ld6}W>beK;*OOcC3`O`i%ebrRjqJh0D=8@uB zjTFAEr%+c8t`a#Fb0iQUna5JDA0k14>C_mLhm!Wgy*mrii!+$9`{BBz$DG7fpzJT_ zt&uKH18TdCXap6CdBM{;emaiZSa&y;kE&eBxzxLP^S*O(**1|`K%-I<7>&Au+pswb$%j)dt(%CR3Q zzd}3Kwo3_Iafam343}}}&}z;7Y0J7wC@52Ww~_4!Pv@isqoQ{=^0}G3=8G()dnX=mqxRO zWummj`;@M5B4eo$-VazDciB#_Bd=97=6!kJos55~sq6lRPeUL;ab(&rz&}0nAWkZ% zKb5?Q?Z(os&R*T`6E7E&tem0C?AFel(XEXoXMH-Y&U`g5&;WOMAMBCdILG~lxKs-M zK9<6bpQ?VoG#siE!o|$r$PUMU3}YNV%JR>w@?e}Y;1AFW=Q*QpPx6FIF>7LB(Qzk1 zP)~uNSJ%xhJTTI!H*XnE+iN7nrj~CcRYRwy*7`b{ z6*Q+fJv$rCDkV|3hMuZ0Kb4HKR&p~Hf_kWrWb&<;l=CsVkp#69uMkj2D(&C~+EXIz z#u?EMCpVn3d#J!CrlKFhNCK49Y^jEAnADDbyw{0t8@^qNT6#FRTU&jX6gY-u%SNM1 zoJ#q;^E0j&4i(jchr#nrXW&8m$2C0Wv{Wf=maa0>FBG*t6X0p`WNDeBqlbFbEQ)O9 z*nD=FDf?E~YHb^HhPe`ROiw|Y38w3q&7VIplW*fDUWmF(6?m#VUlC1kY-{8UVmIh( zhQ>yUw{-!vnsb50@NUtkGy`I6;wD*5DjOzC0VOs{yZ4btae&5&USjib7hVnR&OzZM*wAe3lVa2jGZM*Q^h`n~278&!kFv5dyzQ+Kq-4Uj6x%VVJ9&?LMd* zT0})CQ>HB1$6!@sXHsv)UM*3+(0#r6W)^{(Yc2OrLb-%g0mA%+`9LFZX?33U(=$C^-E=;-5HD8*C-=uMh#@PR{cEduWWuW@kAG;= zPhe87HzQ}2s5PZwx~5SeWjqBESn2JJ9=c0Rc{l?PPhW^qO^2bPpEWn4fK6a~qW=kY zUqw*qKGzegSWJvPC3^|e9Wu_=`RvjE3vxJ$tis>3BMqmPBNlcPC6}d6X7=^ubrzxM z6*_en-va*Z*D}Wqbwhj^kwT*91@GHn=1X|Jr71tIZ;*6T;-^_A;p(&UK2ZV0%qgsgKPj*GB~<^6iqZ4AfY0n_O^;HC)5AUTrLU(F zU@Yz@e>7;kgp;5N&BK03Wr4i2?{Nh7{U8q1%uk9d($bA-X)t>fMoWTdv}D%3=@qKr zOg41QQJj$(*TS%hBt9~sgH0trDv0DqK~y$!Hli^g)OpmNt~yTG08_&9kH=Fw5_+p0 zz-UfB8MbG^=D*KWaG4<_@1Tn}e|?hGciSiI0Y#~=tPSy5a+*|jOAX(=JXByU(eag7 z@+$QB$MF}@M?z?V=E_90F*-LZ>-P+N3|KrJFd@4_H-rZA)`QM=(d{5MvpHTLG4(RgW0HHawY+73^?Rob_V=bFzkR#b7u9YW@b4GQ z2eebruU|pG+~68Wdhqmq)fh;{Ba#cR-&h9t<&8`8HjETN{<%(y5SV;=6UXa2@Gg2h zC#E<3BvqtcK{SqzlM+|qMym++Lid;yj$RKv((9n@BGY&p_|a>?w%|0K;MA8n+4VC$ z-vmI>rS?|4ETX!W3^3nfNEs!X-!e*`_1jf>Qy%K3)4MLpfRHIgsvOdkRL0-J*TuKl5Dv3f^c)7quZKh*BVevh$-GXR`VPy4x)?d|O;g-v zs~ge09FRLF2hw;Sl!1kqAPuL(?-YSX*qS*&^xs@LPu+xqJ;z^fRf&M5xN3(BHnTb% zc*9Xa!5ymN#-aolqMR%#s7{To^@$tVvKovG29AOu)(s?Y;6IfW7+Ft8g~IDo)e-&R zpaE^v{fPrFx_GASTYWnM|JOwt)vIcU+-=kU?t{_{dqz;%)+sxq>rxv31FLwRPlXp? z7OQroGxc(9*V)47M>-eF!2*f_qW*hShCN*abwv#rzqqCYE|GHfq|narTXB$wq8AQB zg{UQ=d`^>m;_FhyoAppebIO3Ql>8bN1jY+ZB9%w{s)u1-N_N4c0!zg!2&niLAdC!M0=O0=8u!G@CBv2Ro{*Q7(<)&^wziKe-$Qt#*{fSiUh?*w7{69N zaRO}1!(W+9QyL~np{bj&fn1?$kB>H?coJ{bNZU?R;{5nBCPn=R$&pmsCT4t77Ev3_ zf0&QUb|ncd=jgiMW{FCe2>}_{HPzkJ&|jD0*{V4w8V;fP_vz8Y+&kLF%C0_K@)_=80k5fb{n$Two zzu~^G0jxejGGoeB;^{cQ3DacJoc@txI2^9WG0iw|R8Gb4S6~X9QE*Dw2^x3iJa*#w zXRUy&1+X-w@)+#86m2C--N;EmRbqlJktyLG%yz8t+Cl~@@k1tK*pTAcqzVbwoO00} ze^|l&_jtS(&j`Q^0$?CRQ^ab%#QBtUdji93f;!We#Ks==b$kxw*~o7HJPrH*vGbm0 z)u`lx;;AZmXiq&%OpPU>7WMha_VI{+noI*l?v~pt(rYAEN@`(iiRtaYs-sdPv!HUb z4hwo`KbR3*RRxJuANzlwKu?h|XG;xh&$D!>7L;4c@F@7j8%)-XN^%@2{v`qzNJ1>A zj8uOj=uYM0|DL`%M2CdgdYksNCyMbZ0sW|)2N}RK6E%PCg^hKnR7tNi75Je>V!os? zy2|Jm8@mT*!zi-CNR?H)br5}vp*7)yxfCmy-H0(b!Vl~|i5lRVM4pWJS&dGds%tTAM*JEzv5JAd^N1d-N?ap)DrL@WyrW-r zoOF)fnFG@hxHsK?@C=jO|KsN~F91LO@sF3^{oU`u->T5h*4fXAz9dTQ*}0AzCpS5v zQEqidnn%)Ty7FC8jso~qXYFCCB-c>wY@5GO3SF>d;c%E78ZFnOq+ASmY7Zi6x%C(O z2~_ymyJtjKSmC%*7`1bZ<+{7&ty1@Cq(8HCdpzBN;dt=?3c&bHe_q~H6={=j{fg$w}1#P^ClRn(r z*Sr5v0DhfQOHt>;Gg|?6V`6kY;l@o~!vC!wQZJ6)DW5K`M-U$G)H^ns1%PQ)2}ydfLIk=xZ(|1BD&l=8OLY#p8o$j*HZx07_}4#thp*qe%_}$8s0x!B!>=NYr7{-y;)|#J@NYh$ zi{=8P3Plajq-83^-agmSrswpy$AZ%ekG3PP-CZ+I3nzW#;WimZzxS-GL%k3Y7JmTf zXhlHH(yM6*yqa&rJQs5VRwZGkq+pXntxD*;^m-9uaS*q@sr{(JRm03V>@W0Mwc>JnWf0yZwyaI)@+80*tVmU@-vhSCv#YOSgX) z>kgzUIH3=o<8{xR3fq)fIlE~Rt((`%k}D#@l&k|-iivjK`{^-tDh zWtuYo<|ps-l~-=?TfcFKA~qZEb7i+w`Yv$$bj_`!fidOQ1sG>Au6w+*5o4JWL%(9R z=>c2WLFLKi#7B>}ymn_}D#vxtlf88UMWL&vUC^erNI5{4dr4NZVaiuwahp zZ067An-_Zvw?u?T+sQ^_FN|ZR?>vKdPz(EgCCVX_0EXUt@2`rR7{~?InY{kqW6mxo ze(&3_0FGUPq(ZFrwtxA~hPOVtz>6c3=~g|}3A$RL^Nf#5bwYFzBQ7WMI8)}Czr5V@ z@_+R?j$`0_J8|oHuz(ZH8OizD#t5N{fxfqhGZmZZcgsR_X5<-t(?qDrl)b=Mm0eL* zL(iZfmI*Y_wIb|gy$?I zk{Oce6 zlC%B7QfOA@X3O>B&6TY(MU0~C1FO4tI6l3_$qO$NS8GJ&Q2u~~)J#v|a+~mTE) zbx3nkLI~^^WnYZWtlrUek=U76->zAK#US_)xlASw@*;G>eh$tHV})I+bX`DD@|+mF zn#X~f?6)+^a>~lMRE93t#vwqjBcUwhv@lH*8ORzC=Q&z;gy@-v>2ue0zy4j%^>hHD zBXHwduKjDCv;D%?yTE>0pb9HFbj!+ADpP{fP0y44!qL&ddefn?W6ZFcCer0d-mHny zZn%{ym%D{dJU&FGWx=&Mzp7Zvtjc~$ELC~^{uw?xPKSU*vEBcYS>}v1*{V6#H0i1| zuYYgEBNu{MVTo|rwErkbGbLGyGPug%&G_S$+Z%r8H}CM`%?+1TnALN+Pkiw46CQqY z&VD=5dAZ{Emt6QiesZ7he&bcvtAT0G^nK*zyT`=vF}YUy^+29ycydX&)042Bd%aq( zWhjXsS_9ewBNMaW`X4UcVG=Gp+mE^@I=jDQBg zi8hVEVH^;8=eQUbhTapjpxF#rsRpdhMHzD?c#o$tE}2d5aMhU?LQ14kDH%f7;Y`0s z&IO!&O+ez?KS<;fK4=l zc7eySBVL}dee{^k@rqSYVrOf)?LM=Lp5S`6^TIS+z{Q*`%%WDKqTW9n=^S*@Oa+!q znhYNZa!~xqm2;36tnGJ>PRxL$_nsi8FF35ye3J^!Sy{Y+XTSf=7y0&Ucj$XZ!7LbzoS6#* z*D;@+AuTC%zT9&BxtFo{lEogV#fqA$33Gn><|BUpw~rX2XSZagR9Fv@DOXl~U~rzk z3r563aoK`OS256a!5ol^;-z^r6f-(muREsMcsgDyH5-^Jjiq>SaVkx{Xq{R`Q>M6T zS%kTgb45{n=%~xiaEV+I&zJu251(Cd`#*NBX9N&^=1oDy#a3p5wYaMGCMh^JohkEf zpY-%cJxyqeQB*T0(deQ_(UF#fW+l|hy7L@u0#h!WPYVujD0J|2!4qNtC%pa91@oK% zrIbee$%U#1t12VPBsHpw34(-S71^u;$D7E|Ip%Cz_(x}xvGAOQeintXC{tF3KJuO4 zxXaK7g7a6lUZQHkr@o_zYBfvg$JUS-2h>$>&eP6Y*UkXdMxA5hZ_?uroVsMtibh7E#Y$7*LdQOfy z)`Me+CN}bFMjnV2I1wRN8&}3;-(wb21=ga>*@{gm%91Nnsw}Pj&$V7{FHELwlH;5T zb!m2JYLVoiW(Kw;c_Wl&Y6@HQsS2nwv=2b zsTl51DijOA6b$LsHuF2@==wnH0z(%$83K2YJ5GAfN$q-77|1f{U{CgSsrK&4fA_k%N@obTzJg`upYV+b&~N?(<( z79z%rD7iKcgcT4`L+9s;#d}2?AySiF8$ZIfBRN}S*<22!DpPsnV#X(3jHGP#VAkv# zy_uRo99jxE!3T$fLq@=JbFOCu5dB22qd|F!h*%5J8wZA+K50YS8|2ki`lWYE(r|% z??NDY*F660_ewRB(L-*7N3K{Y2TFC$l@wtJj`K0IR7)bdGAYi9uIqS`3qv0X&QXNt z&Kc->KtTB5ny&k6pY3S5bupMk)rmcHDOaL*q*Bnu%rQGrrc|wEoD_+LC7-eP!+uFP%p&ZQ^F7Wx z6tnzVwKiOr!h0V);rNSptW&9GXKriGgCx)tl(ihR3(BlUgtFaxvr@zUy8`Fig}-_G z319#GEqqs*LZJpB2S?|OiI|EIoFj;1SxQUorFkpb20+EODzJzzZhnhSZhR=+HTG*0X; zMxqF^TA@-&2n9#*hZW#cjOmIKuTQPAap2~_G-Wzbj$`1+3rm0rD_~#${4IX}J1^s0 zAOwpUTT)?{GQrg=u~ShCCz)$v&dQ(t%>zIgqPY|Wu^D+rd2l}QvM@&tJFS>LaJ*K<8oVC0Qw9V*>V$=+5dAs7dJ@SvUJa$LBv>UgqSSUTaj zCCxg|LA7dtmxKLWp|{oB*?u94(23$}WmyvAl-bXjQYvr$@(Ix?(b@c+bEU*UJs9Pg zh`{*%VnQRQHjAt36;?3r6D3zBjODxT1G}2pO@(j2c9-ve_hpW6o+4UV_A@Cbdhc-v z7yHrPz}32%R4X5!PyFbOPt1rjn(2r-Gf7R@o3V)Ja%p??qHL3G9<1q66YNyIa!wW> zNb^h>f=$*9f0(8O&hiF^U`YHA-+ahF`_{{ZR`QIWSt%kcYJntLTkfHnq!%X$4pFlJ z&&@Jif+vbFX5%d!as&G4=-U>h)QWHEHAR)^P0JmE74Qm_WRY%N^K+Ml**GSF&2<=AS^Z3qBh*ZoA7B^b@aVSc?BUg;Z&)X zuqMu#pz6uQ8y{Wpm6uM*mRx{$6?Ha_)N137m5`@%x^u9o#i|)W$~+bejx3e+@f!6G z&C2C8vFQSR=h&x=hCmgeLs+V^UirodwL(M6_zE#Z#AXT9DWl{sipY&vR3u1VHSSLVq?DjKV7dhSZ?3c_%GRf}RV}DM~ptJX|YgXT@RpfGO zyZ>-lVX5-&gA1mrOjVf|sNxCHv+g}I1eh1{Vn!ZafEZ0#xDSq0tzb<{Wz*9-2s0!S zCw%L*JN)R~CrIzOdDOE{6Z)K%UPD|hkHks8SM=7$%|OQG*Os<*c@2zap^zs}=>aJr5LvdUA2m=6B|F^yfYcL7eeq9!IF!`6Fc)($f~SF zaKnHQ7&z)2S-c6oFju5isKlDcvs#jJUCf!-N3vS6ZC%)x^jSx^ zf8|_D0r>Hce|-ME?|tt&F8!&_yi{(iJ9Fd3T`|vs80t(A#wAhJvyOqwwp)$}Y6uu! zp+i|5OmnilfDpKt5}FIOrP%!Z!%KF_qQun&b0Ln7K z_JuTMF1)Y~f%DOFDwm?H8xZ#2{mvKo_ivf9&`D*#o9TnK`ngncu^IWNZ$IWI?>uJC z6&E~l)e%=cuIs4cS!Jb`%sMFF`|=%LxVgrO8F%hZyY!m=MxQSgBfR|Fg(IONFT`$%3g`z29x!LJ5}A#&supJx+I6-!61 z)e@1DSWqcdnNmjK%YXDoe<(lr!L{ACpXyxK767eP#TL)c?0I-T@`XEVO0~Rr-_~s{ zC)9T33e#d)_I(VD^U|cL_BBddSE(vFS90TP`2aJDz+FQ^? zzL@EQ+TWH6>sgrhW&?KIc}nj&SEW>8o0Q|u@z(t(eEsvc4T-PH-IIae`}#{3DdrrP zOJZJ(TaZd+H)j6yFF)X;v(fep-qFX7^~$pzIy@cVkxux=7jE#|U%5%}1*naUXriaT zc=u!O-sn-a)_GovnU&;*&g1DjNAH@GNvV*nBcdwNo02gG=+=&DQO>h{jn_ZA?BFDD(v`#=?mgPNE}M&wi;~PRb_2FFrbF=mKB8cY~ZOK?En2amoyx zWdV$HW?nLv` z+`j}bNayIQu?kCuX)$t*s^Ybh7n)S5G3>Ja-k7aJ@y^KA-h&K|v$0SL{M*+*;pWkb zn^=tvL8zkCTo{T{5i$-JJH#o`2`eYKWe*7Ig;6816x2-!6=}W>2Rk37Rc#8)g_^Sw zvx+$dPZhR{a_o#~lyYX{ZCh~E`Fl4%-aXUz^{<|5D*$LU_RBMT_EVuy7{n8!C#7sD zJZsYW=H|m$iWR27w2(R$y!OJvZO*P@Mt@6HZErCl`E^x;T58q1bI6a zjs}6=akZefpnFahnCD7A2%7-=WGMTXluZ;KJl^t~_ih;QmJ6X7Ztkb$>ft@xF8t|V ze!!FpJ~-PpV5S>+vFw7H7yj{AZ}VGUevud}o)JlzGH0fxFwGfJxY$iB<4h{HtXTEl zG7tp=J*$PSW;Gl0EVe!k(ing$P%61pmfGIH9tI~Ib&j#Xlq)}a`yv12H}6uN(5WEa zj67?pOj^11>OIy@Y+VN=EZ%E8e?(ofWj4$c%RV8Z1XW4^U!_@&S>d)FztWsVC>6%6 z792Bl?Diuk$0uw$!8Hq~!OJV}Kiodk_w}!y>+1ltW`CUp=+`*oQV0IA(1LI+032P{{6Fe&sdVF$ztc*8Vj77wm;=cOSovy zzs<@G@@CGJzBoi$LXNc00X%#%GR_%^4%Cu%wOB%s1jh#tcl^nZ-ZPu9UpdYR!F!fm zcxhPk-+uQVpMUX~pc8&p>YN!Di_sQfs79Y#wj(iHk(`TiHk*>J4`Rth$u=IPIh!Gd z)JJbB!n$)z>RG>dmy@bj5ou0@ z0cBQjCmW{A9c7wHlQ9vYlEpj=4P}ub;yLjJxV8sSR1;qTq4Ox!SF)yaRbLFWa<*KtEW2qHUvz2n< za6!=OD5bJc$yDnAsyzr7yMWe)z^CLh$BI*wlj1#D}Ty_0Qkt_rCQKr<)#E zm8cb$Eq~w;q*>Z-zZT{G*=R+x=_V*jTFjefQ%j9oZ5d0Oje zZPp>M(+Nwi{Pnw!xwRQkCj^8+m2Jx0tbyWe1LJB_`$?r^WVzg-yF^L3#krdcP;~b9 zC~cKktNpsD6_MILL0V2Zf%1$g%D@8MeCz&ZoR*LVH@ z|FfSn?b#xV`P#>5@!Pr9=1k*FliuF!s>Wyc!CLk!?s*gUJMzVoHKeE*xTkhM_NunVdQVR|pP&emQB;?Ko4^W!%kvTI?W zPAs1=dg0FTK#YMT!d!)q&PE=dPxPIUfB+EBJ{RVq4Bq$?(c1?U4hEoHiGQ|J=JgNG z_}Uk4vx$L{GU4c$P%H{OV=l~2w=hj;N-$+eg_2BvU<%JlaYFC)5FPlG@87%@_eI$+ zmE+C8Zpoak1DylXf>!V`B9l_oI2A`5_p*q%=Yb1weH{SxnQxJHQ$f{HQ7R@1I&j6M zG-{}GW=`2$b&NLbt_VA|7M7fumdsR&snCi6if`OM$2n8tdGVA|EoQ6_Y!_qF-RcC* zLK-VEiMax0I8SiYTZqSHR(8pN#-VHMyJDGlA3WUh!(Tk$3oo7U&cl(P-@o8;&c>-z zL$nVHx#+DY|H|iX@!j9JgLBaP-Z~w`$TVtP_<$)AlNgHrzy0`^s8)`9$EJ^b@#Rxq zxpTw|H&-|@^U|}WkkqWp{@4HeEwdeuuB8FPezpgGu9bOG)=g_r^=V!sB1kK)Z+>{r zD|e3>hMq;ir9>(_oODDF5!5+JmU zxvaIxbIqN`g-8n`M{c`)Dn-FGw&#jq{G(Taxocg{81vvB*U=R2?6 zMx5YUR$d66DrUl|)$F}XX%9y;+MHeBvIyV*=1ct6mu@qNYl*^2#Suf zlXu>K!qCJz3T%Soa;oeTEVbf_x6T8u3gy9hs8%B#{ngtK`DdpaQWo5@kkg2FiT#rW zT{5*4;~+)z?#V@2uR4M=pb}smI#R04OJ-KJO{|ESpZLHKJWHvZt|NWR6wqQLT~){~ zurILbI}{-A6u|#v7c2CI0_iXDV{l7O0uXq7&)i(gV~kac0st9GQYV0gm1iZN)3T&E(EDa zwOjy4#Wk;hB^wP{oB^S!Rs?Ch1BYLCq-@3@0A28Su?X;!qn0jf27Iv9 zlWQQBIsV#utBTnN>{I50#}{n(Gqon-syBm9tVl=`wgfWddR_nkAOJ~3K~!;Mtt?d; zn+B*0jy{^*_f(Y|ryIWc+Fgd=acW(`DteAqftPO|@lU_?GXKNxf0-9<9g)?L`Ar{8 z&{ECaI0jD$MszCG)|Jr<9%T~8(b2%r2Rbk9doj>21heH5fm{+x%4~+f(YgciqysbG z84lo@vNAX`>pWUT`rr+WE0rZxOJA<$Ip|UPM&GSylV)2-5TmVSL-d5;dE>(iwy83w z%9x71p=%}h7W5$&UNToDL|fA?DI52oRHl^KE}2|RLcHmWf?dTki|6>HXSE8(_ece* z73xx9o)=cD-W-}zrcwz`Uip)YPvSM+y1(|hz5)ObuIt*L=InCCg^2n3;8!XweMd|Qx35MS zzH?(k7aU44pTW)(y|akz;BX<(4;{bz?N|BAt2fBS)`wNKEfei?iVJ2G5+Z;7%QG&f zN(zya(}9y!Fd|Kk#3Zy$hhMXC3D&EK;wU1hIC3)q4c-wvglL$;&6CxY zQ*nSREJ854bqgJ<;PJJvOxe~ z{2zUeZ@+fSJ|AKrUcX}msgx@_ zvD)yG;2lE@%|Ah}?8;+vS2{Qh-MX=|0wk7M;9B9c?L1c-idtI&kP70XDf-lrt5CBt zUha6fR~~ImmYZ_9G6yM#eSTXbloq=lyuaFF*yLub+ZY0?A+i}9M}sp0(~{Zl7A|%R z<5b$V$39z53y4{g`xebsRqy_DZa&i-m&3U}EdacAUDy6JCoPq{B>ZX(D7*b0Pap!( z8-hM3HN#G|F5p1KF`bK=d1o;z?Kkc}!L|9iR`cbz>*TAkL2D)U9n$cGtYUASMpE&? z0LE1xtVl?;H!Z@xw6~iIRRrHV)+(f0`TlqAu}jMRM?0zrwN&=IJ-&4@!|5q;eS~&_ zKmYm1++MxHjZmNzoZ3uZTb@CwK&j0o#~#w)&2-eXHjbd)){EF1y@+j5T&=|5IXzl2 zW~K9n_FoyD+KU@7fN+@;!_fvGLgNNom?tRWJ;RM-;%eZn_aE^aw+E(BiK><(=!J@d z=At||%|PEfeCbFn7ePeuYWxOY6|YK2mAWX~F|nUAddO|`#&a--BO?^W7^vAn7pbUh zefJ9R%&T6nkqPKte;!bhX`FH57&gavKsPJWxF>B#cH>O+W(;}ty%-CarSbnQCQL~Z z*gNI@$774Ps??P2&xT;OV?N;BAzmz9dHv^N1{H!(YbH&ZZiqNBAg_;s7z2IqSGHQx zf`n=*q?FNYGxQ&P_a6WA|M?3h*CD;<_~jQ7=O~w3(&e7$1X=ftY35IV^d|rHSMSm} zN9P^EIhMwrFRcqejF=O=$Ehb&6qP~rg7%=w;%BBd#9)fowsn5ByebUwc7lTnpdFj z0!y_mNG^qdVGgR5+%z8ZQYiUvuk;;OtZG^;0L2A_7SM`Pk(Ys={+GKm|9;N36@a<= z_nv7QU^~0X;@$$#xBrR}X0PDH-rS;iF{HefLjtW{vE&vp63m+qE~v+Ae9o|+h_R#Q zL`sD)^weT`;z5MaIkYItvfyH5+>H$D0pA70+oO_80da)bv5ErDlWSqI1WZafwCjR+ zN36=Q+3+vE{|)}^oxN%5+xNNLxW(h+BX)1UN9a6(8BH^PvYYsW@4N~@N!~^eTp*Qd z^j@$xvh&~@p{O>8pT=AA)qbxQj8hL3H3JZJ_Ub>xoJ-rdxZp{-(hnW3`+wPc((1|Ye2Iw6+V0kUdjrq_0d6DCkdzE&Bk3XL5oD&f06zn7F(32`>?g0Uz(F<26}NNfZ3gw1_!v=7ncR&7yy)L4UdD8JY&Z^5xgKm~Qw#|LX1A zT6O^L$AR^9V!gWIipoha)_Kp{AKviz;fZ@9nlKE3u=d8KC=sDqqZMwSFQnM@6MTHy zOkA;7jVA6{uvRZu_G@LxkrcZ98PaWUl+EY3t!^P2z9-g?4+ z^Sh57XD)(>1D8ZxpHT4NY2vENDMzBf6hOv-WnP)9Pb0?OB;3dm+vO<7Vy+Rnw$;BM z9msWTv_`RS{8In}y|4iA-S2+4eDj;%?5woAyLN|_akC}j+;>-z~aXv(Z2CXnACH5e7IX4C~LXJ!#Jbtj{8?WEz?|yPDQ;pHC*kqXZ z%4#msIR)XbfAXB|=>)-Dkt`~N>>{e;O(b-zW448UwwRr;sB=s zBcw(kV#gD_86u5hN(_$W7y{(&z+tHzN<*VK>=%b@7ko@xdV@2D;9{mRQ>yoZ1hBxD zzx${E<`}K+?0TV{O4DW%)18h0S92pLBe4P%)&nfnZ}>juo3iWGx2{ZEO{s(V|9BUWd$jvf6uy-k!vk19YYYJe|zk?oMFDYD-$)WB_X)395nXUd5byazN4BPj{BI=VB3e;OVEFEjwCs)cyB z)1=)}*oYEsg}TDL(rRPw^xk1i?|rA0}u_YxuWm7a&oevsj=*44nsgjU|v`bg;tc@Rksq#)BVa2 z15dA>a&I$$H2&pRBLDtRE?JgBP-QU3#K)Lei!$UuGvoR7%Ao1=(4w+b1eliDM`|3-C);h)4CAwQ})V1*LlN;p71?nYK%!!;LhOn%zxmcP} zYU|9uJ|}l1;!j1WAJ8Y2o#GfwGujk zDFmU`!c8q~#>{p*P)eg{BbX~C4bkPsWAJgRw#qn7*il_|82##yOVIW%Kmtk3NO&-b%erfMv;QPleaYlwu?Gd}!j;lagy+QogI zUOj`kQFb%SEpsrTwGvVS0?YnDo<;;^lZ;d=Si`EHdP#83z+okyj9e|ssR$oFz2V_D zkZPqajkPLOtb>z4nz110^`0j;H=GPGY2j-xpYqo~_|VmE1+oaGlW&^3%JH9n{{t@0 zN0!;$boZsQu1+e7DYDFk*IvEPYp*_}Eh|5n8-M)P6aKr`A5+CJfn}Llm(|CQ`lRbpkvjL+PqiAEl-;<^-5~EX= zG0fzZ@BH`wb@e^Syq4pQ}w`mxOg*x_O2B51M#7EGZFlLd+$|on9TV0BOvwRk#-7&~*{bz5EO%hDa^$bQF@fPh)nnkoBgYlk_5QynJyB zKTbH~ag0ZrHtgnw(`jHzk$GORaO~o9>tG~+b-cwfWm(mz>er! zeVIFscX`2RC0&8di@k>b|Fxeh>(Y?wC+eXnDQB7*L5zo|0~Y8TS0E}k7^PLV=MVVN z_domy7}VE!PE4nXo%B=Mx<|bP(m2rS+?#tsQDaqCwMk}}bbq@dT;0r2DyfguRu$g* z;F|yAyFX$KaFPRGedU}tUO8vXnSC`vR|5WzKX`(a;-{*0Yi@G~

co@VIU0wUsN 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 Ef(F#BDF6Tf literal 0 HcmV?d00001 diff --git a/files/opencs/raster/startup/small/create-addon.png b/files/opencs/raster/startup/small/create-addon.png new file mode 100644 index 0000000000000000000000000000000000000000..64fd138be55a6d2c73bff277da82b8f72996d707 GIT binary patch literal 1714 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 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/startup/small/new-game.png b/files/opencs/raster/startup/small/new-game.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7d14c5580ef04b7b9fb4c31cc29bddfc42f683 GIT binary patch literal 2122 zcmV-Q2(|Z#P)6z)7^~}yb?2R{e6en?F=Rr_n zDUp@H0XRSoIVBvzi4%ew5{eLEbpj!T@WA)J`BJm7XQ5-G zqT`II@Az_VVLGV~-amci+dnz+2k!EwV)FjyUU>GU)$quw!C`y;LKop zX8){rOjj%O6J7vqVUYLF@2;x|xWgmyfF@*vTZ3hfDR~Z)$38cAVyLXjSCol5$)jqGh_z2!R zTG}xbim@#b9^y!{nK_!xsV@L`1dulm0;P^m3dXsmjv)#H8Wn|(A_}Rve07`EUdm{5 zhjUjpJ^@n^VGv?uKv0QD>LCkBKyK{*yDg!-btDIqf>BnI%rqHV!AMk?t4Vw|=EVCS z^VnjOm%sQZm#+2s`?UeOvzMvqy@XQn$#+I9HZ)D`^&4M%?*3f? zN@J2qfzJ!7QNUbPvc5UQTFYCfuhZ|2IeWFosmq&qV;HPl=4U5A#v_R1h^b~wEMciv zkQ>i-F??cI0PkIIQqavxhSqT-^UPEfxe;96*ujLF^?{)`8nd*rMQ2Zi?u|YxTYaKd zjnX(aN0!o+q)IW}NNRTixTOjdbSDL-uxRH%1@!6(=g)jdzn2mSfwLu_KQzP1i<{H~ z!ScxSn?If>)D;?4OHxUwlY&87Qc)lX%zGbVZHWv5zW)6E zT(~~KIZwM;r5*^brGj^tx|q^3kP+)fl3It!%!9iF`0~=EAT2DJhdi}hx;Eg0t%7zS zcyuP_mw*0%Yd40x^5P*5OxJk+ky!+u)9+v5AD3>>Xht;l)M-VYq*B=(ptSDXq`)~( zS~ygodF#w7DhwIL3CEu~#L*-BxzS5mTj|o8PMF`9JBf-s$Ji2IuM^0SB=Y?JuNTON8S^#4&)>d^R?E~Xn$&?;8rew5q{iBu zEYjfpo?RVy^WBTLEdju0HzSEcI)THDN35^+IkVj7@>aq15uh}smjvF?juk>F7TY1c z{+O!p7$;Cs6ukcGQ@8Di6J_YVz~lX`&033>l9#`9jDww+y@{q;39!yFGgBi-s-Pr= zEdhuFIMS(+Xs|&@b-IBoJ*)lf@p}S%0^xAZeHt8KZRBWm=80{OxoSXJl#GWtk@ReA zjtKLVipl92LDzuBGvA8IgvM&g&PESW6Er z4P+2;_|XHjrecytg`Dw@#czD@nDYun2zr&N9Yp?%&%&g#ki<6OwdP zkc~5(6u0e$x931<{bUXlMTrm+SqX3^M=Q+>kIi#&BjYcpmZ?S|VXeZBbsSu5k+ka+ zBESRz9!2Rv2Aa(H#mRWL{SylBjrU&PoCCwmP*}rgW1B*P^`8B6O%B{QW@~$gPAeg< z#>ArJfmVg?MjxD`=o?BQDML-I7LX6i*6vGyFK%|gd(SwxBue14W>88Rp~OkaGY6a0 zv_LqE9}N*oQH?{&!lKHOpb}6C1hKR9w#GP!zV+o}v-bp$UgS3i2gnP{w((T7Vk!yQ zN=r=XNaBcx7N-d%*i!K1107-|2ttho3NINMM~OrV&=PED_TLl03%@g2hNbhQqntd; zSnCc6B;X+kB>QF((lnn;7z6@OvadrZ1x^Tbs7abtv=pe)KX7jcU^rQZ07DB# zfD{5D9M)L8aYRyZU#rf^iyMTIrWyx`W|Pu+GHVH<3h~S|#tE|0*{QP{{9RL6T07*qoM6N<$f_W47 ATL1t6 literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/startup/configure.svgz b/files/opencs/scalable/startup/configure.svgz new file mode 100644 index 0000000000000000000000000000000000000000..1275ec53fa7735b89e41fb58f9ef17d80dc54f58 GIT binary patch literal 5808 zcmV;h7EkFPiwFP!000000PS4caw1ojefL-7$S*|a)pj_d96d3%yXVb; zAf;lAKmn+v^4HH==Kz@rDL^u9m!n67U9j_9_jT=a$=82|MX&hbAKw;#d%T;?m&Jcg?r-|vO^Y%S|B?H=C|oH-@zwo!GAgVz zCcG+&e*ev9pS@muxcRIo3Jh?%xR@?3&)zSWcNgd9(?vO8f8}s?d;Y`VL-oyU{?F>_ z9D_eUt80vgn;Lib^GP|I-<*$z=hdXTt)|Nbipys4VO@MUuLjHUhYIcA&d?icP8Xjy z3eQK^o6-*t59NbSg=CEJT&Q!U`Y6#~e4H)^kNtL4%&M!Z5<;BgSzYqDQWuYt@${D` z2a}#QdxvE2@ZYAwnourg_w!+Ojha}Q!M5987DJqoChe!j29H}99}H#9}aO#Lvf z9=@7AUY?0U6jH_#vxU+@=Ui@OhMuk`vxm#G597u7YLdIJrUQKKUk!%8+{|b9Q*=J9 z9*UE|X{&dz_rL>A|x z!H4na{B5=PWjVV$uSakqQ0q5)B&rVg>Y|&x-RG^&vX?kllUQ)W>;yc5hzu%V4s@ zhc(a9^Q0Q0)xqRp@NuzeI@$ci`*{T)^?3)!nhb4sp|y6K$2TkDPt)-d&gdRW^8-!c z?^Ec{PN^T~gXsd^30Xlr{AlWi`rLt-Y?Pon`0EX35$oH0oKL{GadNefRcfDStkDS|ekVzrUG*rBqf} z;mJg0tkF7uGeO9nTzyl$KKW`r{9~^V)AJ$(uWKeh%F8o@(jmaRZKxkX4QNeBC+Z?IwOJ{6N`v{tIrM2Fp(g zx_QD%ySdRJ)+N7A2FvP;zAl9VLxZgPs)rxeS%L57)gIllFr8=W7DexUX${<%Y>6HU z5nOOCv_u~Q6e4KvTEf?|)Iuv|cVyWmmk!2j#m^r3u9=J}h7MV7x+%T&8d@Co_jgdx zb$?zwN2qOX9N=6xmpdhLoHRM=l@oG5@ADr=DcUxZ*F1^p`OJO7Sa&74?nDar0uhz~ ze-XaTbaq!O$1^1Qq7V@Fg@6>!PZa_%DdCN?uGg=>rf=Y2XngC-Pe5sbE`gwvMhPr}r_8J-lXfm1mT>yRNxwUwsA zUAI?TyvoJ4&_S!*ei{AV)3irS69M_CX|~G!uG2hVBC4?j|Nka(D&0}fP1;YSVZUbz z$Z4JqWdB>_fO}Ub$j?7w74lEIJh6A#jJ*6K)+dhP`nG)nwCEbAWP=(D$y zPK32iJk7oPDxBsg86rN#jg^C(JmE198kZH`rsE#z7f)&Rz!jK6^cn!pN)JTkKYhL( zG_>csu76Qd)1}Y=!pa0{@%mf8i)d`Aq!R?sq7*>~sbBrxY&5$odcVz(53l=4>EwFh z4*}p$;PelH5_39$_mmCybqQQ`%MS z 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/startup/create-addon.svgz b/files/opencs/scalable/startup/create-addon.svgz new file mode 100644 index 0000000000000000000000000000000000000000..75425667bd585384f586187f527c4df353e0e536 GIT binary patch literal 116154 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/startup/edit-content.svgz b/files/opencs/scalable/startup/edit-content.svgz new file mode 100644 index 0000000000000000000000000000000000000000..049f1e8132179dd371494be6e5b48d48ab61da69 GIT binary patch literal 120969 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

#a|xGwn`1907B`L83| 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°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 HA;J9*><|ai literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/startup/new-game.svgz b/files/opencs/scalable/startup/new-game.svgz new file mode 100644 index 0000000000000000000000000000000000000000..e81571911160d7cb7b2cb4cc31514a291861e982 GIT binary patch literal 113710 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�=zFFI>=8};vAg0)56PqSi=3akS><7 z^L(}s$VsEgjdbh$Y@P)hH^&DLWChv z*%_7S6{!V+2pt*cUE*C-ei(HPSMq9$@Hgx9d=AQHpk;1I=CCT6~OlL#jA z0HQ?9>M(BUm%iB)`P{9&1hxqm*hV=C-U+?zI+d?FpS2??97VOhO|(7AbK}hvwVyLG z+$6w;dEP`ITx{!e#ddU9;!R#xcf9r+dG0offLF9cd%Dz3vWDE9A*YYX1Z;>zC|RS ziqh7w7Nbd)U3$dMY{wmfj4{{wBD)B@%62VQxfiI zkym-)AJFY?-!jf{Rb6)+QSnZ;9$J7%&BzV#Oo_AH@HIa^G-%FiRumpW&t9<0(32N7 zn?|LtmMynbdp`gHr`V{=e2(*LMBMY;xab zikJCE5o^W^8`}P!CTE-UM2sHhBfSrVgZv=LaRXw^RvIyB$J(_pXR|oDF?%@rJqZR& zn-nZ`4sLus%BP`kdeJDbTa_Koz9r^CSy=2US9ERf9U7!kh&YNsO)^ZUW>&-MW9Xc4 z+duj<>_92^V30}h9@Xv7$u*`6j`Z_gI%Nd4$I~F{caFgMAk9ymN>Q z-qKiMii@5R*#y$?kfGLEOguLInt)Wo=0XZV0h?blL36xqa-8#O#KzNk9545~1Sc-- z(xcel%$-KjOb7Q@OepJgdnBd})b$2-Nd5v5C&^99r%sZ_8W$!~LVL&@3d0^BH@ZvX+6B7p2$!=gaewN(ZVU4}kpsDn!9(2;mt zRV(G&`sMAZKkXf7*A@1mO z-c5N6UC%~=-V_7qlzW*k>k&Pb)s`!abT19r;E;TW@Vuo5E8@%s6=*BcO-i~Vd)Myw zdbUBWq>v@ugyjRYozYsmUTgd(aP>h-{=QOmkplELqlWu9Qc`N3*V@#VQ`gS z2^AtRa0Y-9RwgqKY(Gt|u>cJh=jzUrTPWYbLtkXQR}RwgO5nwLxf*;_A;Dovnq`7lr!c?+>UFR z`0B#yWH+-&5d)90FY~)yNjzZj+mdLJv(z$o)=th@hgz_DuN~?LMT#K$(&KP3kNDAZ z`3=i>*GJjhglguBHIti6i7_F4U?cwkD=u=v3^rqqR&^E!2V#rf2+lP-#3=XB0V8%G zm2&r@Z^}WEBgQ#FH>L1ALOG+>YHTB-FO_chaCWHJ$uxw}RmF$X^k+nqu zm@3FWb~tZAr7wH#VIG_Vcj^kI7bZi4n*$~dIdeY}n;Zk#h*a$K3)N5Y?YiM@R7UKX zgj0I8*X+bP{kR2f^lJghu5BYa4jkO`C@j_a{4%U-nqNoDGQyKeXgx8Ws2yrVo6tIM z#sY1oAiZ%0j+~DwUf0_yydHK|DsLcHxqA)HNQP%uC$`4h+XZmRJ35`JHq^+A+5v4+ z&hsM`NdZYSUi7TGDr=si$bn@Ae>bMO-k0T7=b++DJFvHl+Q>colER9HEDhliKhy`2 z7ZQtsX}hW$kU=vOZ-2O@DYHGeSZ_cxJJsaJIJt${9BKLpv2N=!`)#Bwx*qHvVlP}h z5Jhj=b+IMZ6=FjwS|p2`bM4>2{J1oAg6s31S%X%TICL@jR44IbVTB9rchz|;1pKIV zT;3(9dBOa{{o&xeZA5x@A3eUcJy%SnzB)PeTp} z;M~pjB1G5h;|qB&LO*@&j< zvQYrG)TZAEs~B#wk-(}udP{kqgYC!4^0*+-k~X6K

G2kL$xgx{{ZPQh&d>dxlBW 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

    wzp5Y?gpqEME4Sa|~K3#kH#0ulou6z3z&L`%;8 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 aGl%H|i5~okw-){RcmD>+1@2M|hywt&9pOp< literal 0 HcmV?d00001 From 25b7cd33eab7c8d039bdfef1d706538b166b3861 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 8 Sep 2013 14:31:20 +0200 Subject: [PATCH 187/194] added FileWidget; fixed OpenCS configuration --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/editor.cpp | 54 +++++++++++++++++++---- apps/opencs/editor.hpp | 3 ++ apps/opencs/main.cpp | 4 +- apps/opencs/view/doc/filewidget.cpp | 54 +++++++++++++++++++++++ apps/opencs/view/doc/filewidget.hpp | 42 ++++++++++++++++++ apps/opencs/view/doc/newgame.cpp | 44 +++++++++++++++++- apps/opencs/view/doc/newgame.hpp | 21 ++++++++- components/files/configurationmanager.cpp | 14 +++++- components/files/configurationmanager.hpp | 4 +- 10 files changed, 225 insertions(+), 17 deletions(-) create mode 100644 apps/opencs/view/doc/filewidget.cpp create mode 100644 apps/opencs/view/doc/filewidget.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 8b9e31e3a..916ae6e24 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -43,7 +43,7 @@ opencs_units_noqt (model/tools opencs_units (view/doc - viewmanager view operations operation subview startup filedialog newgame + viewmanager view operations operation subview startup filedialog newgame filewidget ) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index afbe233e4..09e80d73b 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "model/doc/document.hpp" #include "model/world/data.hpp" @@ -25,6 +26,9 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) connect (&mFileDialog, SIGNAL(openFiles()), this, SLOT(openFiles())); connect (&mFileDialog, SIGNAL(createNewFile()), this, SLOT(createNewFile())); + connect (&mNewGame, SIGNAL (createRequest (const QString&)), + this, SLOT (createNewGame (const QString&))); + setupDataFiles(); } @@ -43,26 +47,39 @@ void CS::Editor::setupDataFiles() mCfgMgr.readConfiguration(variables, desc); - Files::PathContainer mDataDirs, mDataLocal; + Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { - mDataDirs = Files::PathContainer(variables["data"].as()); + dataDirs = Files::PathContainer(variables["data"].as()); } std::string local = variables["data-local"].as(); if (!local.empty()) { - mDataLocal.push_back(Files::PathContainer::value_type(local)); + dataLocal.push_back(Files::PathContainer::value_type(local)); } - mCfgMgr.processPaths(mDataDirs); - mCfgMgr.processPaths(mDataLocal); + mCfgMgr.processPaths (dataDirs); + mCfgMgr.processPaths (dataLocal, true); + + if (!dataLocal.empty()) + mLocal = dataLocal[0]; + else + { + QMessageBox messageBox; + messageBox.setWindowTitle (tr ("No local data path available")); + messageBox.setIcon (QMessageBox::Critical); + messageBox.setStandardButtons (QMessageBox::Ok); + messageBox.setText(tr("
    OpenCS is unable to access the local data directory. This may indicate a faulty configuration or a broken install.")); + messageBox.exec(); + + QApplication::exit (1); + return; + } // Set the charset for reading the esm/esp files QString encoding = QString::fromStdString(variables["encoding"].as()); mFileDialog.setEncoding(encoding); - Files::PathContainer dataDirs; - dataDirs.insert(dataDirs.end(), mDataDirs.begin(), mDataDirs.end()); - dataDirs.insert(dataDirs.end(), mDataLocal.begin(), mDataLocal.end()); + dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); for (Files::PathContainer::const_iterator iter = dataDirs.begin(); iter != dataDirs.end(); ++iter) { @@ -73,7 +90,6 @@ void CS::Editor::setupDataFiles() //load the settings into the userSettings instance. const QString settingFileName = "opencs.cfg"; CSMSettings::UserSettings::instance().loadSettings(settingFileName); - } void CS::Editor::createGame() @@ -133,6 +149,23 @@ void CS::Editor::createNewFile() mFileDialog.hide(); } +void CS::Editor::createNewGame (const QString& file) +{ + boost::filesystem::path path (mLocal); + + path /= file.toUtf8().data(); + + std::vector files; + + files.push_back (path); + + CSMDoc::Document *document = mDocumentManager.addDocument (files, true); + + mViewManager.addView (document); + + mNewGame.hide(); +} + void CS::Editor::showStartup() { if(mStartup.isHidden()) @@ -173,6 +206,9 @@ void CS::Editor::connectToIPCServer() int CS::Editor::run() { + if (mLocal.empty()) + return 1; + mStartup.show(); QApplication::setQuitOnLastWindowClosed (true); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 7d86358b3..d9e1ca6bc 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -35,6 +35,8 @@ namespace CS FileDialog mFileDialog; Files::ConfigurationManager mCfgMgr; + boost::filesystem::path mLocal; + void setupDataFiles(); // not implemented @@ -59,6 +61,7 @@ namespace CS void loadDocument(); void openFiles(); void createNewFile(); + void createNewGame (const QString& file); void showStartup(); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index eddeb1983..ef7123c20 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -41,8 +41,8 @@ int main(int argc, char *argv[]) if(!editor.makeIPCServer()) { - editor.connectToIPCServer(); - return 0; + editor.connectToIPCServer(); + return 0; } return editor.run(); diff --git a/apps/opencs/view/doc/filewidget.cpp b/apps/opencs/view/doc/filewidget.cpp new file mode 100644 index 000000000..bcd6b83f2 --- /dev/null +++ b/apps/opencs/view/doc/filewidget.cpp @@ -0,0 +1,54 @@ + +#include "filewidget.hpp" + +#include +#include +#include +#include +#include + +QString CSVDoc::FileWidget::getExtension() const +{ + return mAddon ? ".omwaddon" : ".omwgame"; +} + +CSVDoc::FileWidget::FileWidget (QWidget *parent) : QWidget (parent), mAddon (false) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + mInput = new QLineEdit (this); + mInput->setValidator (new QRegExpValidator(QRegExp("^[a-zA-Z0-9\\s]*$"))); + + layout->addWidget (mInput, 1); + + mType = new QLabel (this); + + layout ->addWidget (mType); + + connect (mInput, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); + + setLayout (layout); +} + +void CSVDoc::FileWidget::setType (bool addon) +{ + mAddon = addon; + + mType->setText (getExtension()); +} + +QString CSVDoc::FileWidget::getName() const +{ + QString text = mInput->text(); + + if (text.isEmpty()) + return ""; + + return text + getExtension(); +} + +void CSVDoc::FileWidget::textChanged (const QString& text) +{ + emit stateChanged (!text.isEmpty()); + emit nameChanged (getName()); +} \ No newline at end of file diff --git a/apps/opencs/view/doc/filewidget.hpp b/apps/opencs/view/doc/filewidget.hpp new file mode 100644 index 000000000..58d52db92 --- /dev/null +++ b/apps/opencs/view/doc/filewidget.hpp @@ -0,0 +1,42 @@ +#ifndef CSV_DOC_FILEWIDGET_H +#define CSV_DOC_FILEWIDGET_H + +#include + +class QLabel; +class QString; +class QLineEdit; + +namespace CSVDoc +{ + class FileWidget : public QWidget + { + Q_OBJECT + + bool mAddon; + QLineEdit *mInput; + QLabel *mType; + + QString getExtension() const; + + public: + + FileWidget (QWidget *parent = 0); + + void setType (bool addon); + + QString getName() const; + + private slots: + + void textChanged (const QString& text); + + signals: + + void stateChanged (bool valid); + + void nameChanged (const QString& file); + }; +} + +#endif diff --git a/apps/opencs/view/doc/newgame.cpp b/apps/opencs/view/doc/newgame.cpp index eea0823a7..7ff0f38dc 100644 --- a/apps/opencs/view/doc/newgame.cpp +++ b/apps/opencs/view/doc/newgame.cpp @@ -3,12 +3,54 @@ #include #include +#include +#include +#include + +#include "filewidget.hpp" CSVDoc::NewGameDialogue::NewGameDialogue() { setWindowTitle ("Create New Game"); + QVBoxLayout *layout = new QVBoxLayout (this); + + mFileWidget = new FileWidget (this); + mFileWidget->setType (false); + + layout->addWidget (mFileWidget, 1); + + QDialogButtonBox *buttons = new QDialogButtonBox (this); + + mCreate = new QPushButton ("Create", this); + mCreate->setDefault (true); + mCreate->setEnabled (false); + + buttons->addButton (mCreate, QDialogButtonBox::AcceptRole); + + QPushButton *cancel = new QPushButton ("Cancel", this); + + buttons->addButton (cancel, QDialogButtonBox::RejectRole); + + layout->addWidget (buttons); + + setLayout (layout); + + connect (mFileWidget, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); + connect (mCreate, SIGNAL (clicked()), this, SLOT (create())); + connect (cancel, SIGNAL (clicked()), this, SLOT (reject())); + QRect scr = QApplication::desktop()->screenGeometry(); QRect rect = geometry(); move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); -} \ No newline at end of file +} + +void CSVDoc::NewGameDialogue::stateChanged (bool valid) +{ + mCreate->setEnabled (valid); +} + +void CSVDoc::NewGameDialogue::create() +{ + emit createRequest (mFileWidget->getName()); +} diff --git a/apps/opencs/view/doc/newgame.hpp b/apps/opencs/view/doc/newgame.hpp index 7c9e92a76..5e581e417 100644 --- a/apps/opencs/view/doc/newgame.hpp +++ b/apps/opencs/view/doc/newgame.hpp @@ -1,17 +1,34 @@ #ifndef CSV_DOC_NEWGAME_H #define CSV_DOC_NEWGAME_H -#include +#include + +class QPushButton; namespace CSVDoc { - class NewGameDialogue : public QWidget + class FileWidget; + + class NewGameDialogue : public QDialog { Q_OBJECT + QPushButton *mCreate; + FileWidget *mFileWidget; + public: NewGameDialogue(); + + signals: + + void createRequest (const QString& file); + + private slots: + + void stateChanged (bool valid); + + void create(); }; } diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 56e55a98d..75c877dc5 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -56,7 +56,7 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m } -void ConfigurationManager::processPaths(Files::PathContainer& dataDirs) +void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, bool create) { std::string path; for (Files::PathContainer::iterator it = dataDirs.begin(); it != dataDirs.end(); ++it) @@ -94,6 +94,18 @@ void ConfigurationManager::processPaths(Files::PathContainer& dataDirs) if (!boost::filesystem::is_directory(*it)) { + if (create) + { + try + { + boost::filesystem::create_directories (*it); + } + catch (...) {} + + if (boost::filesystem::is_directory(*it)) + continue; + } + (*it).clear(); } } diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 765f1cebf..4df871664 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -30,7 +30,9 @@ struct ConfigurationManager void readConfiguration(boost::program_options::variables_map& variables, boost::program_options::options_description& description); - void processPaths(Files::PathContainer& dataDirs); + + void processPaths(Files::PathContainer& dataDirs, bool create = false); + ///< \param create Try creating the directory, if it does not exist. /**< Fixed paths */ const boost::filesystem::path& getGlobalPath() const; From 2dc3c0ae4f931c2fa464bd18755c03c344c164a9 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 9 Sep 2013 09:41:43 +0200 Subject: [PATCH 188/194] integrated startup icons --- apps/opencs/view/doc/startup.cpp | 11 +++++------ files/opencs/resources.qrc | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 010fbaf16..4cc64f2df 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -41,13 +41,13 @@ QWidget *CSVDoc::StartupDialogue::createButtons() mLayout = new QGridLayout (widget); /// \todo add icons - QPushButton *loadDocument = addButton ("Edit A Content File", QIcon ("")); + QPushButton *loadDocument = addButton ("Edit A Content File", QIcon (":startup/edit-content")); connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); - QPushButton *createAddon = addButton ("Create A New Addon", QIcon ("")); + QPushButton *createAddon = addButton ("Create A New Addon", QIcon (":startup/create-addon")); connect (createAddon, SIGNAL (clicked()), this, SIGNAL (createAddon())); - QPushButton *createGame = addButton ("Create A New Game", QIcon ("")); + QPushButton *createGame = addButton ("Create A New Game", QIcon (":startup/create-game")); connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); for (int i=0; i<3; ++i) @@ -69,7 +69,6 @@ QWidget *CSVDoc::StartupDialogue::createButtons() return widget; } -#include QWidget *CSVDoc::StartupDialogue::createTools() { QWidget *widget = new QWidget (this); @@ -81,11 +80,11 @@ QWidget *CSVDoc::StartupDialogue::createTools() QPushButton *config = new QPushButton (widget); config->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - config->setIcon (style()->standardIcon (QStyle::SP_FileDialogStart)); /// \todo replace icon + config->setIcon (QIcon (":startup/configure")); layout->addWidget (config); - layout->addWidget (new QWidget, 1); // dummy icon; stops buttons from taking all the space + layout->addWidget (new QWidget, 1); // dummy widget; stops buttons from taking all the space widget->setLayout (layout); diff --git a/files/opencs/resources.qrc b/files/opencs/resources.qrc index 321413763..56e25b2c1 100644 --- a/files/opencs/resources.qrc +++ b/files/opencs/resources.qrc @@ -25,5 +25,9 @@ repair.png static.png weapon.png + raster/startup/big/create-addon.png + raster/startup/big/new-game.png + raster/startup/big/edit-content.png + raster/startup/small/configure.png From e9f14449eb9286425a3f0e6cc8306a78bdf803a1 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 10 Sep 2013 16:45:01 +0200 Subject: [PATCH 189/194] added AdjusterWidget --- apps/opencs/CMakeLists.txt | 1 + apps/opencs/editor.cpp | 29 ++++---- apps/opencs/editor.hpp | 2 +- apps/opencs/model/doc/document.cpp | 15 ++-- apps/opencs/model/doc/document.hpp | 9 ++- apps/opencs/model/doc/documentmanager.cpp | 4 +- apps/opencs/model/doc/documentmanager.hpp | 3 +- apps/opencs/view/doc/adjusterwidget.cpp | 90 +++++++++++++++++++++++ apps/opencs/view/doc/adjusterwidget.hpp | 41 +++++++++++ apps/opencs/view/doc/filewidget.cpp | 3 +- apps/opencs/view/doc/filewidget.hpp | 4 +- apps/opencs/view/doc/newgame.cpp | 16 +++- apps/opencs/view/doc/newgame.hpp | 11 ++- apps/opencs/view/doc/view.cpp | 2 +- 14 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 apps/opencs/view/doc/adjusterwidget.cpp create mode 100644 apps/opencs/view/doc/adjusterwidget.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 916ae6e24..aa6f6ba76 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -44,6 +44,7 @@ opencs_units_noqt (model/tools opencs_units (view/doc viewmanager view operations operation subview startup filedialog newgame filewidget + adjusterwidget ) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 09e80d73b..9a6832ec0 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -9,10 +9,15 @@ #include "model/doc/document.hpp" #include "model/world/data.hpp" + CS::Editor::Editor() : mViewManager (mDocumentManager) { mIpcServerName = "org.openmw.OpenCS"; + setupDataFiles(); + + mNewGame.setLocalData (mLocal); + connect (&mViewManager, SIGNAL (newGameRequest ()), this, SLOT (createGame ())); connect (&mViewManager, SIGNAL (newAddonRequest ()), this, SLOT (createAddon ())); connect (&mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); @@ -26,10 +31,8 @@ CS::Editor::Editor() : mViewManager (mDocumentManager) connect (&mFileDialog, SIGNAL(openFiles()), this, SLOT(openFiles())); connect (&mFileDialog, SIGNAL(createNewFile()), this, SLOT(createNewFile())); - connect (&mNewGame, SIGNAL (createRequest (const QString&)), - this, SLOT (createNewGame (const QString&))); - - setupDataFiles(); + connect (&mNewGame, SIGNAL (createRequest (const boost::filesystem::path&)), + this, SLOT (createNewGame (const boost::filesystem::path&))); } void CS::Editor::setupDataFiles() @@ -126,7 +129,9 @@ void CS::Editor::openFiles() files.push_back(path.toStdString()); } - CSMDoc::Document *document = mDocumentManager.addDocument(files, false); + /// \todo Get the save path from the file dialogue + + CSMDoc::Document *document = mDocumentManager.addDocument (files, *files.rbegin(), false); mViewManager.addView (document); mFileDialog.hide(); @@ -143,23 +148,21 @@ void CS::Editor::createNewFile() files.push_back(mFileDialog.fileName().toStdString()); - CSMDoc::Document *document = mDocumentManager.addDocument (files, true); + /// \todo Get the save path from the file dialogue. + + CSMDoc::Document *document = mDocumentManager.addDocument (files, *files.rbegin(), true); mViewManager.addView (document); mFileDialog.hide(); } -void CS::Editor::createNewGame (const QString& file) +void CS::Editor::createNewGame (const boost::filesystem::path& file) { - boost::filesystem::path path (mLocal); - - path /= file.toUtf8().data(); - std::vector files; - files.push_back (path); + files.push_back (file); - CSMDoc::Document *document = mDocumentManager.addDocument (files, true); + CSMDoc::Document *document = mDocumentManager.addDocument (files, file, true); mViewManager.addView (document); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index d9e1ca6bc..c83b2a685 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -61,7 +61,7 @@ namespace CS void loadDocument(); void openFiles(); void createNewFile(); - void createNewGame (const QString& file); + void createNewGame (const boost::filesystem::path& file); void showStartup(); diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 30e9c21d1..d7138f671 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -2139,18 +2139,13 @@ void CSMDoc::Document::createBase() } } -CSMDoc::Document::Document (const std::vector& files, bool new_) -: mTools (mData) +CSMDoc::Document::Document (const std::vector& files, + const boost::filesystem::path& savePath, bool new_) +: mSavePath (savePath), mTools (mData) { if (files.empty()) throw std::runtime_error ("Empty content file sequence"); - /// \todo adjust last file name: - /// \li make sure it is located in the data-local directory (adjust path if necessary) - /// \li make sure the extension matches the new scheme (change it if necesarry) - - mName = files.back().filename().string(); - if (new_ && files.size()==1) createBase(); else @@ -2201,9 +2196,9 @@ int CSMDoc::Document::getState() const return state; } -const std::string& CSMDoc::Document::getName() const +const boost::filesystem::path& CSMDoc::Document::getSavePath() const { - return mName; + return mSavePath; } void CSMDoc::Document::save() diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index 1c6d9db2a..3532721ea 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -31,7 +31,7 @@ namespace CSMDoc private: - std::string mName; ///< \todo replace name with ESX list + boost::filesystem::path mSavePath; CSMWorld::Data mData; CSMTools::Tools mTools; @@ -64,15 +64,16 @@ namespace CSMDoc public: - Document (const std::vector& files, bool new_); + Document (const std::vector& files, + const boost::filesystem::path& savePath, bool new_); + ~Document(); QUndoStack& getUndoStack(); int getState() const; - const std::string& getName() const; - ///< \todo replace with ESX list + const boost::filesystem::path& getSavePath() const; void save(); diff --git a/apps/opencs/model/doc/documentmanager.cpp b/apps/opencs/model/doc/documentmanager.cpp index 740c0b582..b079109ea 100644 --- a/apps/opencs/model/doc/documentmanager.cpp +++ b/apps/opencs/model/doc/documentmanager.cpp @@ -14,10 +14,10 @@ CSMDoc::DocumentManager::~DocumentManager() delete *iter; } -CSMDoc::Document *CSMDoc::DocumentManager::addDocument (const std::vector& files, +CSMDoc::Document *CSMDoc::DocumentManager::addDocument (const std::vector& files, const boost::filesystem::path& savePath, bool new_) { - Document *document = new Document (files, new_); + Document *document = new Document (files, savePath, new_); mDocuments.push_back (document); diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index a307b76a5..dfded8d5c 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -23,7 +23,8 @@ namespace CSMDoc ~DocumentManager(); - Document *addDocument (const std::vector& files, bool new_); + Document *addDocument (const std::vector& files, + const boost::filesystem::path& savePath, bool new_); ///< The ownership of the returned document is not transferred to the caller. /// /// \param new_ Do not load the last content file in \a files and instead create in an diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp new file mode 100644 index 000000000..24e37b0f5 --- /dev/null +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -0,0 +1,90 @@ + +#include "adjusterwidget.hpp" + +#include + +#include + +#include +#include +#include + +CSVDoc::AdjusterWidget::AdjusterWidget (QWidget *parent) +: QWidget (parent), mValid (false) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + mIcon = new QLabel (this); + + layout->addWidget (mIcon, 0); + + mMessage = new QLabel (this); + mMessage->setWordWrap (true); + mMessage->setSizePolicy (QSizePolicy (QSizePolicy::Minimum, QSizePolicy::Minimum)); + + layout->addWidget (mMessage, 1); + + setName ("", false); + + setLayout (layout); +} + +void CSVDoc::AdjusterWidget::setLocalData (const boost::filesystem::path& localData) +{ + mLocalData = localData; +} + +boost::filesystem::path CSVDoc::AdjusterWidget::getPath() const +{ + if (!mValid) + throw std::logic_error ("invalid content file path"); + + return mResultPath; +} + +void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) +{ + QString message; + + if (name.isEmpty()) + { + mValid = false; + message = "No name."; + } + else + { + boost::filesystem::path path (name.toUtf8().data()); + + path.replace_extension (addon ? ".omwaddon" : ".omwgame"); + + if (path.parent_path().string()==mLocalData.string()) + { + // path already points to the local data directory + message = QString::fromUtf8 (("Will be saved as: " + path.native()).c_str()); + mResultPath = path; + mValid = true; + } + else + { + // path points somewhere else or is a leaf name. + path = mLocalData / path.filename(); + + message = QString::fromUtf8 (("Will be saved as: " + path.native()).c_str()); + mResultPath = path; + mValid = true; + + if (boost::filesystem::exists (path)) + { + /// \todo add an user setting to make this an error. + message += "

    But a file with the same name already exists. If you continue, it will be overwritten."; + } + } + } + + mMessage->setText (message); + mIcon->setPixmap (style()->standardIcon ( + mValid ? QStyle::SP_MessageBoxInformation : QStyle::SP_MessageBoxWarning). + pixmap (QSize (16, 16))); + + emit stateChanged (mValid); +} \ No newline at end of file diff --git a/apps/opencs/view/doc/adjusterwidget.hpp b/apps/opencs/view/doc/adjusterwidget.hpp new file mode 100644 index 000000000..f578dc4ae --- /dev/null +++ b/apps/opencs/view/doc/adjusterwidget.hpp @@ -0,0 +1,41 @@ +#ifndef CSV_DOC_ADJUSTERWIDGET_H +#define CSV_DOC_ADJUSTERWIDGET_H + +#include + +#include + +class QLabel; + +namespace CSVDoc +{ + class AdjusterWidget : public QWidget + { + Q_OBJECT + + boost::filesystem::path mLocalData; + QLabel *mMessage; + QLabel *mIcon; + bool mValid; + boost::filesystem::path mResultPath; + + public: + + AdjusterWidget (QWidget *parent = 0); + + void setLocalData (const boost::filesystem::path& localData); + + boost::filesystem::path getPath() const; + ///< This function must not be called if there is no valid path. + + public slots: + + void setName (const QString& name, bool addon); + + signals: + + void stateChanged (bool valid); + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/view/doc/filewidget.cpp b/apps/opencs/view/doc/filewidget.cpp index bcd6b83f2..c8f33e92d 100644 --- a/apps/opencs/view/doc/filewidget.cpp +++ b/apps/opencs/view/doc/filewidget.cpp @@ -49,6 +49,5 @@ QString CSVDoc::FileWidget::getName() const void CSVDoc::FileWidget::textChanged (const QString& text) { - emit stateChanged (!text.isEmpty()); - emit nameChanged (getName()); + emit nameChanged (getName(), mAddon); } \ No newline at end of file diff --git a/apps/opencs/view/doc/filewidget.hpp b/apps/opencs/view/doc/filewidget.hpp index 58d52db92..c51c29632 100644 --- a/apps/opencs/view/doc/filewidget.hpp +++ b/apps/opencs/view/doc/filewidget.hpp @@ -33,9 +33,7 @@ namespace CSVDoc signals: - void stateChanged (bool valid); - - void nameChanged (const QString& file); + void nameChanged (const QString& file, bool addon); }; } diff --git a/apps/opencs/view/doc/newgame.cpp b/apps/opencs/view/doc/newgame.cpp index 7ff0f38dc..98681c499 100644 --- a/apps/opencs/view/doc/newgame.cpp +++ b/apps/opencs/view/doc/newgame.cpp @@ -8,6 +8,7 @@ #include #include "filewidget.hpp" +#include "adjusterwidget.hpp" CSVDoc::NewGameDialogue::NewGameDialogue() { @@ -20,6 +21,10 @@ CSVDoc::NewGameDialogue::NewGameDialogue() layout->addWidget (mFileWidget, 1); + mAdjusterWidget = new AdjusterWidget (this); + + layout->addWidget (mAdjusterWidget, 1); + QDialogButtonBox *buttons = new QDialogButtonBox (this); mCreate = new QPushButton ("Create", this); @@ -36,15 +41,22 @@ CSVDoc::NewGameDialogue::NewGameDialogue() setLayout (layout); - connect (mFileWidget, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); + connect (mAdjusterWidget, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); connect (mCreate, SIGNAL (clicked()), this, SLOT (create())); connect (cancel, SIGNAL (clicked()), this, SLOT (reject())); + connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), + mAdjusterWidget, SLOT (setName (const QString&, bool))); QRect scr = QApplication::desktop()->screenGeometry(); QRect rect = geometry(); move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } +void CSVDoc::NewGameDialogue::setLocalData (const boost::filesystem::path& localData) +{ + mAdjusterWidget->setLocalData (localData); +} + void CSVDoc::NewGameDialogue::stateChanged (bool valid) { mCreate->setEnabled (valid); @@ -52,5 +64,5 @@ void CSVDoc::NewGameDialogue::stateChanged (bool valid) void CSVDoc::NewGameDialogue::create() { - emit createRequest (mFileWidget->getName()); + emit createRequest (mAdjusterWidget->getPath()); } diff --git a/apps/opencs/view/doc/newgame.hpp b/apps/opencs/view/doc/newgame.hpp index 5e581e417..aa97682ff 100644 --- a/apps/opencs/view/doc/newgame.hpp +++ b/apps/opencs/view/doc/newgame.hpp @@ -1,13 +1,19 @@ #ifndef CSV_DOC_NEWGAME_H #define CSV_DOC_NEWGAME_H +#include + #include +#include + +Q_DECLARE_METATYPE (boost::filesystem::path) class QPushButton; namespace CSVDoc { class FileWidget; + class AdjusterWidget; class NewGameDialogue : public QDialog { @@ -15,14 +21,17 @@ namespace CSVDoc QPushButton *mCreate; FileWidget *mFileWidget; + AdjusterWidget *mAdjusterWidget; public: NewGameDialogue(); + void setLocalData (const boost::filesystem::path& localData); + signals: - void createRequest (const QString& file); + void createRequest (const boost::filesystem::path& file); private slots: diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index b2e848aaa..7183753e1 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -184,7 +184,7 @@ void CSVDoc::View::updateTitle() { std::ostringstream stream; - stream << mDocument->getName(); + stream << mDocument->getSavePath().filename().string(); if (mDocument->getState() & CSMDoc::State_Modified) stream << " *"; From 09e692f1f62081dfe6ff3f81113d03f4a255ef13 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 10 Sep 2013 22:11:57 +0200 Subject: [PATCH 190/194] Fixed polish font --- apps/openmw/mwgui/fontloader.cpp | 55 +++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwgui/fontloader.cpp b/apps/openmw/mwgui/fontloader.cpp index ff160105a..7a6317c16 100644 --- a/apps/openmw/mwgui/fontloader.cpp +++ b/apps/openmw/mwgui/fontloader.cpp @@ -9,6 +9,7 @@ #include #include + #include namespace @@ -62,6 +63,58 @@ namespace return unicode; } + + std::string getUtf8 (unsigned char c, ToUTF8::Utf8Encoder& encoder, ToUTF8::FromType encoding) + { + if (encoding == ToUTF8::WINDOWS_1250) + { + unsigned char win1250; + std::map conv; + conv[0x80] = 0xc6; + conv[0x81] = 0x9c; + conv[0x82] = 0xe6; + conv[0x83] = 0xb3; + conv[0x84] = 0xf1; + conv[0x85] = 0xb9; + conv[0x86] = 0xbf; + conv[0x87] = 0x9f; + conv[0x88] = 0xea; + conv[0x89] = 0xea; + conv[0x8a] = 0x0; // not contained in win1250 + conv[0x8b] = 0x0; // not contained in win1250 + conv[0x8c] = 0x8f; + conv[0x8d] = 0xaf; + conv[0x8e] = 0xa5; + conv[0x8f] = 0x8c; + conv[0x90] = 0xca; + conv[0x93] = 0xa3; + conv[0x94] = 0xf6; + conv[0x95] = 0xf3; + conv[0x96] = 0xaf; + conv[0x97] = 0x8f; + conv[0x99] = 0xd3; + conv[0x9a] = 0xd1; + conv[0x9c] = 0x0; // not contained in win1250 + conv[0xa0] = 0xb9; + conv[0xa1] = 0xaf; + conv[0xa2] = 0xf3; + conv[0xa3] = 0xbf; + conv[0xa4] = 0x0; // not contained in win1250 + conv[0xe1] = 0x8c; + conv[0xe1] = 0x8c; + conv[0xe3] = 0x0; // not contained in win1250 + conv[0xf5] = 0x0; // not contained in win1250 + + if (conv.find(c) != conv.end()) + win1250 = conv[c]; + else + win1250 = c; + return encoder.getUtf8(std::string(1, win1250)); + } + else + return encoder.getUtf8(std::string(1, c)); + } + } namespace MWGui @@ -184,7 +237,7 @@ namespace MWGui int h = data[i].bottom_left.y*height - y1; ToUTF8::Utf8Encoder encoder(mEncoding); - unsigned long unicodeVal = utf8ToUnicode(encoder.getUtf8(std::string(1, (unsigned char)(i)))); + unsigned long unicodeVal = utf8ToUnicode(getUtf8(i, encoder, mEncoding)); MyGUI::xml::ElementPtr code = codes->createChild("Code"); code->addAttribute("index", unicodeVal); From 07ff0a8de9fe21f74d89f03b198363a71131b060 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 12 Sep 2013 20:52:05 +0200 Subject: [PATCH 191/194] possible build fix for Windows --- apps/opencs/view/doc/adjusterwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp index 24e37b0f5..910819700 100644 --- a/apps/opencs/view/doc/adjusterwidget.cpp +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -2,6 +2,7 @@ #include "adjusterwidget.hpp" #include +#include #include From 6f64b1b9dbb0d0560af246509ad8dad300119013 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 13 Sep 2013 15:11:17 +0200 Subject: [PATCH 192/194] cmake option to use system tinyxml instead of embedded one --- CMakeLists.txt | 13 +++++++++++++ extern/oics/CMakeLists.txt | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f95f2d4e..2ec306e5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,19 @@ if (NOT FFMPEG_FOUND) message(WARNING "--------------------") endif (NOT FFMPEG_FOUND) +# TinyXML +option(USE_SYSTEM_TINYXML "Use system TinyXML library instead of internal." OFF) +if(USE_SYSTEM_TINYXML) + find_library(TINYXML_LIBRARIES tinyxml) + find_path(TINYXML_INCLUDE_DIR tinyxml.h) + message(STATUS "Found TinyXML: ${TINYXML_LIBRARIES} ${TINYXML_INCLUDE_DIR}") + if(TINYXML_LIBRARIES AND TINYXML_INCLUDE_DIR) + include_directories(${TINYXML_INCLUDE_DIR}) + message(STATUS "Using system TinyXML library.") + else() + message(FATAL_ERROR "Detection of system TinyXML incomplete.") + endif() +endif() # Platform specific if (WIN32) diff --git a/extern/oics/CMakeLists.txt b/extern/oics/CMakeLists.txt index 7c14387a4..5c1edbf62 100644 --- a/extern/oics/CMakeLists.txt +++ b/extern/oics/CMakeLists.txt @@ -9,12 +9,23 @@ set(OICS_SOURCE_FILES ICSInputControlSystem_keyboard.cpp ICSInputControlSystem_mouse.cpp ICSInputControlSystem_joystick.cpp +) + +set(TINYXML_SOURCE_FILES tinyxml.cpp tinyxmlparser.cpp tinyxmlerror.cpp - tinystr.cpp + tinystr.cpp ) -add_library(${OICS_LIBRARY} STATIC ${OICS_SOURCE_FILES}) +if(USE_SYSTEM_TINYXML) + add_library(${OICS_LIBRARY} STATIC ${OICS_SOURCE_FILES}) + target_link_libraries(${OICS_LIBRARY} ${TINYXML_LIBRARIES}) +else() + add_library(${OICS_LIBRARY} STATIC + ${OICS_SOURCE_FILES} + ${TINYXML_SOURCE_FILES}) +endif() +# Does this do anything? link_directories(${CMAKE_CURRENT_BINARY_DIR}) From 9637e1641be22db6b77fb5aff0f8068b51106de6 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 14 Sep 2013 13:33:49 +0200 Subject: [PATCH 193/194] fix for building with system tinyxml --- CMakeLists.txt | 1 + apps/openmw/CMakeLists.txt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ec306e5a..625239aa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,7 @@ if(USE_SYSTEM_TINYXML) find_library(TINYXML_LIBRARIES tinyxml) find_path(TINYXML_INCLUDE_DIR tinyxml.h) message(STATUS "Found TinyXML: ${TINYXML_LIBRARIES} ${TINYXML_INCLUDE_DIR}") + add_definitions (-DTIXML_USE_STL) if(TINYXML_LIBRARIES AND TINYXML_INCLUDE_DIR) include_directories(${TINYXML_INCLUDE_DIR}) message(STATUS "Using system TinyXML library.") diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index a44fd4b34..b367e2a1e 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -119,6 +119,10 @@ target_link_libraries(openmw components ) +if (USE_SYSTEM_TINYXML) + target_link_libraries(openmw ${TINYXML_LIBRARIES}) +endif() + if (NOT UNIX) target_link_libraries(openmw ${SDL2MAIN_LIBRARY}) endif() From d5ef843f5615c309d784794028179eee6d21cd0d Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 16 Sep 2013 19:22:08 +0200 Subject: [PATCH 194/194] Fix a missing model update --- apps/openmw/mwgui/hud.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index f9d31bdcd..edcd49738 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -250,6 +250,7 @@ namespace MWGui // remove object from the container it was coming from mDragAndDrop->mSourceModel->removeItem(mDragAndDrop->mItem, mDragAndDrop->mDraggedCount); mDragAndDrop->finish(); + mDragAndDrop->mSourceModel->update(); } else {