diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000000..54966fdfb0 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,50 @@ +name: CMake + +on: + pull_request: + branches: [ master ] + +env: + BUILD_TYPE: RelWithDebInfo + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Add OpenMW PPA Dependancies + run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update + + - name: Install Building Dependancies + run: sudo CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic + + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M + + - name: Configure + run: cmake -S . -B . -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS='-Werror' -DCMAKE_CXX_FLAGS="-Werror -Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" + + - name: Build + run: cmake --build . --config ${{env.BUILD_TYPE}} --parallel 3 + + - name: Install + shell: bash + run: cmake --install . + + - name: Create Artifact + shell: bash + working-directory: install + run: | + ls -laR + 7z a ../build_artifact.7z . + + - name: Upload Artifact + uses: actions/upload-artifact@v1 + with: + path: ./build_artifact.7z + name: build_artifact.7z diff --git a/AUTHORS.md b/AUTHORS.md index 62121a797b..2080f12a99 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -80,6 +80,7 @@ Programmers Federico Guerra (FedeWar) Fil Krynicki (filkry) Finbar Crago (finbar-crago) + Florent Teppe (Tetramir) Florian Weber (Florianjw) Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) @@ -228,6 +229,7 @@ Programmers Yuri Krupenin zelurker Noah Gooder + Andrew Appuhamy (andrew-app) Documentation ------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index b1132a9e68..54ded2a9bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.48.0 ------ + Bug #1751: Birthsign abilities increase modified attribute values instead of base ones Bug #3246: ESSImporter: Most NPCs are dead on save load Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) @@ -10,20 +11,25 @@ Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4700: Editor: Incorrect command implementation Bug #4744: Invisible particles must still be processed - Bug #4752: UpdateCellCommand doesn't undo properly Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system + Bug #5207: Loose summons can be present in scene Bug #5379: Wandering NPCs falling through cantons Bug #5453: Magic effect VFX are offset for creatures Bug #5483: AutoCalc flag is not used to calculate spells cost Bug #5508: Engine binary links to Qt without using it - Bug #5755: Active grid object paging - disappearing textures + Bug #5596: Effects in constant spells should not be merged + Bug #5621: Drained stats cannot be restored + Bug #5766: Active grid object paging - disappearing textures Bug #5788: Texture editing parses the selected indexes wrongly + Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention Bug #5842: GetDisposition adds temporary disposition change from different actors + Bug #5863: GetEffect should return true after the player has teleported Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown Bug #6067: esp loader fails in for certain subrecord orders + Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed Bug #6115: Showmap overzealous matching @@ -34,23 +40,41 @@ Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6172: Some creatures can't open doors Bug #6174: Spellmaking and Enchanting sliders differences from vanilla Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla Bug #6197: Infinite Casting Loop + Bug #6273: Respawning NPCs rotation is inconsistent + Bug #6282: Laura craft doesn't follow the player character + Bug #6283: Avis Dorsey follows you after her death + Bug #6289: Keyword search in dialogues expected the text to be all ASCII characters + Bug #6291: Can't pickup the dead mage's journal from the mysterious hunter mod + Bug #6302: Teleporting disabled actor breaks its disabled state + Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken + Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house + Feature #890: OpenMW-CS: Column filtering Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console Feature #3616: Allow Zoom levels on the World Map + Feature #4297: Implement APPLIED_ONCE flag for magic effects + Feature #4414: Handle duration of EXTRA SPELL magic effect Feature #4595: Unique object identifier - Feature #4737: Handle instance move from one cell to another + Feature #5198: Implement "Magic effect expired" event + Feature #5454: Clear active spells from actor when he disappears from scene Feature #5489: MCP: Telekinesis fix for activators + Feature #5737: Handle instance move from one cell to another Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving Feature #6032: Reverse-z depth buffer Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly Feature #6199: Support FBO Rendering + Feature #6249: Alpha testing support for Collada Feature #6251: OpenMW-CS: Set instance movement based on camera zoom + Feature #6288: Preserve the "blocked" record flag for referenceable objects. + Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings Task #6264: Remove the old classes in animation.cpp + 0.47.0 ------ @@ -184,6 +208,7 @@ Bug #6043: Actor can have torch missing when torch animation is played Bug #6047: Mouse bindings can be triggered during save loading Bug #6136: Game freezes when NPCs try to open doors that are about to be closed + Bug #6294: Game crashes with empty pathgrid Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references Feature #1536: Show more information about level on menu @@ -213,6 +238,7 @@ Feature #5519: Code Patch tab in launcher Feature #5524: Resume failed script execution after reload Feature #5545: Option to allow stealing from an unconscious NPC during combat + Feature #5551: Do not reboot PC after OpenMW installation on Windows Feature #5563: Run physics update in background thread Feature #5579: MCP SetAngle enhancement Feature #5580: Service refusal filtering @@ -227,6 +253,7 @@ Feature #5814: Bsatool should be able to create BSA archives, not only to extract it Feature #5828: Support more than 8 lights Feature #5910: Fall back to delta time when physics can't keep up + Feature #5980: Support Bullet with double precision instead of one with single precision Feature #6024: OpenMW-CS: Selecting terrain in "Terrain land editing" should support "Add to selection" and "Remove from selection" modes Feature #6033: Include pathgrid to navigation mesh Feature #6034: Find path based on area cost depending on NPC stats diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index bc0eb0013d..5d20fa75ce 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -32,6 +32,7 @@ declare -a CMAKE_CONF_OPTS=( -DCMAKE_INSTALL_PREFIX=install -DCMAKE_C_FLAGS='-Werror' -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" + -DOPENMW_CXX_FLAGS="-Werror=implicit-fallthrough" ) if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then diff --git a/CMakeLists.txt b/CMakeLists.txt index 52a824d4e9..2564379847 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,11 @@ if(POLICY CMP0083) cmake_policy(SET CMP0083 NEW) endif() +# to link with freetype library +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF) if(OPENMW_GL4ES_MANUAL_INIT) add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) @@ -202,8 +207,6 @@ if (USE_QT) find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5OpenGL REQUIRED) - # Instruct CMake to run moc automatically when needed. - #set(CMAKE_AUTOMOC ON) endif() set(USED_OSG_COMPONENTS @@ -540,6 +543,10 @@ if (BUILD_OPENCS) add_subdirectory (extern/osgQt) endif() +if (OPENMW_CXX_FLAGS) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OPENMW_CXX_FLAGS}") +endif() + # Components add_subdirectory (components) diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 46e4426dc5..81b9711bbf 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -124,11 +124,9 @@ public: { mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; mContext->mPlayerBase = npc; - ESM::SpellState::SpellParams empty; // FIXME: player start spells and birthsign spells aren't listed here, // need to fix openmw to account for this - for (const auto & spell : npc.mSpells.mList) - mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList; // Clear the list now that we've written it, this prevents issues cropping up with // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index e9ec906e6a..7e8bd67e94 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -36,23 +36,6 @@ set(LAUNCHER_HEADER ) # Headers that must be pre-processed -set(LAUNCHER_HEADER_MOC - datafilespage.hpp - graphicspage.hpp - maindialog.hpp - playpage.hpp - textslotmsgbox.hpp - settingspage.hpp - advancedpage.hpp - - utils/cellnameloader.hpp - utils/textinputdialog.hpp - utils/profilescombobox.hpp - utils/lineedit.hpp - utils/openalutil.hpp - -) - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -74,7 +57,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) -QT5_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -109,4 +91,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw-launcher gcov) endif() +if(USE_QT) + set_property(TARGET openmw-launcher PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 2e81885c75..cb6205ef5c 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -52,11 +52,8 @@ void readVFS(VFS::Archive* anArchive,std::string archivePath = "") myManager.addArchive(anArchive); myManager.buildIndex(); - std::map files=myManager.getIndex(); - for(auto it=files.begin(); it!=files.end(); ++it) + for(const auto& name : myManager.getRecursiveDirectoryIterator("")) { - std::string name = it->first; - try{ if(isNIF(name)) { diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 4b3b8030e6..952bbbdbda 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -8,11 +8,11 @@ opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) -opencs_units_noqt (model/doc +opencs_units (model/doc stage savingstate savingstages blacklist messages ) -opencs_hdrs_noqt (model/doc +opencs_hdrs (model/doc state ) @@ -23,14 +23,14 @@ opencs_units (model/world ) -opencs_units_noqt (model/world +opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) -opencs_hdrs_noqt (model/world +opencs_hdrs (model/world columnimp idcollection collection info subcellcollection ) @@ -39,14 +39,14 @@ opencs_units (model/tools tools reportmodel mergeoperation ) -opencs_units_noqt (model/tools +opencs_units (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck ) -opencs_hdrs_noqt (model/tools +opencs_hdrs (model/tools mergestate ) @@ -57,11 +57,11 @@ opencs_units (view/doc ) -opencs_units_noqt (view/doc +opencs_units (view/doc subviewfactory ) -opencs_hdrs_noqt (view/doc +opencs_hdrs (view/doc subviewfactoryimp ) @@ -71,10 +71,10 @@ opencs_units (view/world cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator - bodypartcreator landtexturecreator landcreator + bodypartcreator landtexturecreator landcreator tableheadermouseeventhandler ) -opencs_units_noqt (view/world +opencs_units (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils @@ -92,12 +92,12 @@ opencs_units (view/render cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) -opencs_units_noqt (view/render +opencs_units (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) -opencs_hdrs_noqt (view/render +opencs_hdrs (view/render mask ) @@ -106,7 +106,7 @@ opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) -opencs_units_noqt (view/tools +opencs_units (view/tools subviews ) @@ -119,11 +119,11 @@ opencs_units (model/prefs shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting ) -opencs_units_noqt (model/prefs +opencs_units (model/prefs category ) -opencs_units_noqt (model/filter +opencs_units (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) @@ -150,7 +150,6 @@ if(WIN32) endif(WIN32) qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -qt5_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt5_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) # for compiled .ui files @@ -259,3 +258,7 @@ endif (MSVC) if(APPLE) INSTALL(TARGETS openmw-cs BUNDLE DESTINATION "." COMPONENT Bundle) endif() + +if(USE_QT) + set_property(TARGET openmw-cs PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 9da2c2c144..a410d34b2a 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -285,8 +285,7 @@ void CSMDoc::WriteCellCollectionStage::writeReferences (const std::deque& r { refRecord.mRefNum.mIndex = newRefNum++; } - - if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) + else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) != stream.str() && !interior) { // An empty mOriginalCell is meant to indicate that it is the same as diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index d59a1efe5e..9ccd1772e1 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -108,7 +108,7 @@ namespace CSMDoc state == CSMWorld::RecordBase::State_ModifiedOnly || state == CSMWorld::RecordBase::State_Deleted) { - writer.startRecord (record.sRecordId); + writer.startRecord (record.sRecordId, record.mRecordFlags); record.save (writer, state == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index d6066aa04d..6eefdb6e21 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -371,6 +371,7 @@ namespace CSMWorld { ColumnId_Skill7, "Skill 7" }, { ColumnId_Persistent, "Persistent" }, + { ColumnId_Blocked, "Blocked" }, { -1, 0 } // end marker }; diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 16482d4898..8cf02b46ac 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -344,6 +344,7 @@ namespace CSMWorld ColumnId_FactionAttrib2 = 312, ColumnId_Persistent = 313, + ColumnId_Blocked = 314, // 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/commanddispatcher.cpp b/apps/opencs/model/world/commanddispatcher.cpp index 3da3e3d22c..c5d78b6586 100644 --- a/apps/opencs/model/world/commanddispatcher.cpp +++ b/apps/opencs/model/world/commanddispatcher.cpp @@ -3,9 +3,6 @@ #include #include -#include -#include - #include #include @@ -142,31 +139,12 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons if (mLocked) return; - std::unique_ptr clonedData; - std::unique_ptr deleteData; - - std::string newId; - - std::unique_ptr modifyData; std::unique_ptr modifyCell; - QAbstractItemModel* sourceModel = model; - if (IdTableProxyModel* proxy = dynamic_cast (model)) - sourceModel = proxy->sourceModel(); - - CSMWorld::IdTable& table = dynamic_cast(*sourceModel); // for getId() - int stateColumn = table.findColumnIndex(Columns::ColumnId_Modification); - QModelIndex stateIndex = table.getModelIndex(table.getId(index.row()), stateColumn); - RecordBase::State state = static_cast (sourceModel->data(stateIndex).toInt()); int columnId = model->data (index, ColumnBase::Role_ColumnId).toInt(); - // This is not guaranteed to be the same as \a model, since a proxy could be used. - IdTable& model2 = dynamic_cast (*mDocument.getData().getTableModel(mId)); - - // DeleteCommand triggers a signal to the whole row from IdTable::setData(), so ignore the second call - if (state != RecordBase::State_Deleted && - (columnId==Columns::ColumnId_PositionXPos || columnId==Columns::ColumnId_PositionYPos)) + if (columnId==Columns::ColumnId_PositionXPos || columnId==Columns::ColumnId_PositionYPos) { const float oldPosition = model->data (index).toFloat(); @@ -178,9 +156,12 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons int row = proxy ? proxy->mapToSource (index).row() : index.row(); + // This is not guaranteed to be the same as \a model, since a proxy could be used. + IdTable& model2 = dynamic_cast (*mDocument.getData().getTableModel (mId)); + int cellColumn = model2.searchColumnIndex (Columns::ColumnId_Cell); - if (cellColumn != -1) + if (cellColumn!=-1) { QModelIndex cellIndex = model2.index (row, cellColumn); @@ -188,46 +169,23 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons if (cellId.find ('#')!=std::string::npos) { - RefCollection& collection = mDocument.getData().getReferences(); - newId = collection.getNewId(); - - clonedData.reset(new CloneCommand(table, - table.getId(row), - newId, - CSMWorld::UniversalId::Type_Reference)); - - deleteData.reset(new DeleteCommand(table, - table.getId(row), - CSMWorld::UniversalId::Type_Reference)); + // Need to recalculate the cell + modifyCell.reset (new UpdateCellCommand (model2, row)); } } } } - if (!clonedData.get()) - { - // DeleteCommand will trigger executeModify after setting the state to State_Deleted - // from CommandDelegate::setModelDataImp() - ignore - if (state != RecordBase::State_Deleted) - modifyData.reset(new CSMWorld::ModifyCommand(*model, index, new_)); - } + std::unique_ptr modifyData ( + new CSMWorld::ModifyCommand (*model, index, new_)); - if (clonedData.get()) + if (modifyCell.get()) { CommandMacro macro (mDocument.getUndoStack()); - macro.push(clonedData.release()); - macro.push(deleteData.release()); - - // cannot do these earlier because newIndex is not available until CloneCommand is executed - QModelIndex newIndex = model2.getModelIndex (newId, index.column()); - modifyData.reset (new CSMWorld::ModifyCommand (*model, newIndex, new_)); - macro.push(modifyData.release()); - - // once the data is updated update the cell location - modifyCell.reset(new UpdateCellCommand(model2, newIndex.row())); - macro.push(modifyCell.release()); + macro.push (modifyData.release()); + macro.push (modifyCell.release()); } - else if (!clonedData.get() && modifyData.get()) + else mDocument.getUndoStack().push (modifyData.release()); } diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 6d3882c015..5b4a9b31bc 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -123,6 +123,14 @@ Qt::ItemFlags CSMWorld::IdTable::flags (const QModelIndex & index) const if (mIdCollection->getColumn (index.column()).isUserEditable()) flags |= Qt::ItemIsEditable; + int blockedColumn = searchColumnIndex(Columns::ColumnId_Blocked); + if (blockedColumn != -1 && blockedColumn != index.column()) + { + bool isBlocked = mIdCollection->getData(index.row(), blockedColumn).toInt(); + if (isBlocked) + flags = Qt::ItemIsSelectable; // not enabled (to grey out) + } + return flags; } diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 4f56bbb463..c6f5148290 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -91,12 +91,48 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool else ref.mCell = cell2.mId; + if (ref.mRefNum.mContentFile != -1 && !base) + { + ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; + ref.mRefNum.mIndex &= 0x00ffffff; + } + unsigned int refNum = (ref.mRefNum.mIndex & 0x00ffffff) | (ref.mRefNum.hasContentFile() ? ref.mRefNum.mContentFile : 0xff) << 24; std::map::iterator iter = cache.find(refNum); - if (ref.mRefNum.mContentFile != -1 && !base) ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; + if (isMoved) + { + if (iter == cache.end()) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, + mCells.getId(cellIndex)); + + messages.add(id, "Attempt to move a non-existent reference - RefNum index " + + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/"", + CSMDoc::Message::Severity_Warning); + continue; + } + + int index = getIntIndex(iter->second); + + // ensure we have the same record id for setRecord() + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId); + + std::unique_ptr > record(new Record); + // TODO: check whether a base record be moved + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); + + // overwrite original record + setRecord(index, std::move(record)); + + continue; // NOTE: assumed moved references are not deleted at the same time + } if (isDeleted) { diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index 34e258c11b..affd66af32 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -26,7 +26,7 @@ namespace CSMWorld class RefCollection : public Collection { Collection& mCells; - std::map mRefIndex; + std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum int mNextId; diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index 1d1b5a94a6..c35d3c5a7c 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -25,6 +25,9 @@ namespace CSMWorld const RefIdColumn *mId; const RefIdColumn *mModified; const RefIdColumn *mType; + const RefIdColumn *mBlocked; + + BaseColumns () : mBlocked(nullptr) {} }; /// \brief Base adapter for all refereceable record types @@ -90,6 +93,9 @@ namespace CSMWorld if (column==mBase.mType) return static_cast (mType); + if (column==mBase.mBlocked) + return (record.get().mRecordFlags & ESM::FLAG_Blocked) != 0; + return QVariant(); } @@ -102,6 +108,17 @@ namespace CSMWorld if (column==mBase.mModified) record.mState = static_cast (value.toInt()); + else if (column==mBase.mBlocked) + { + RecordT record2 = record.get(); + + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Blocked; + else + record2.mRecordFlags &= ~ESM::FLAG_Blocked; + + record.setModified(record2); + } } template @@ -110,6 +127,14 @@ namespace CSMWorld return mType; } + // NOTE: Body Part should not have persistence (but BodyPart is not listed in the Objects + // table at the moment). + // + // Spellmaking - not persistent - currently not part of objects table + // Enchanting - not persistent - currently not part of objects table + // + // Leveled Creature - no model, so not persistent + // Leveled Item - no model, so not persistent struct ModelColumns : public BaseColumns { @@ -1845,6 +1870,7 @@ namespace CSMWorld content.mWander.mDuration = static_cast(value.toInt()); else return; // return without saving + break; case 3: if (content.mType == ESM::AI_Wander) content.mWander.mTimeOfDay = static_cast(value.toInt()); diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 71629694c0..928c7284ad 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -50,13 +50,16 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Blocked, ColumnBase::Display_Boolean, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); + baseColumns.mBlocked = &mColumns.back(); ModelColumns modelColumns (baseColumns); - mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); - modelColumns.mModel = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Persistent, ColumnBase::Display_Boolean); modelColumns.mPersistence = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); + modelColumns.mModel = &mColumns.back(); NameColumns nameColumns (modelColumns); diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index 2544886f3e..cd9f58e848 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -23,10 +23,8 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const * size_t baseSize = mBaseDirectory.size(); - const std::map& index = vfs->getIndex(); - for (std::map::const_iterator it = index.begin(); it != index.end(); ++it) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator("")) { - std::string filepath = it->first; if (filepath.size()selectedFiles().back(); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 4514cfb585..c89437d70d 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -65,6 +65,8 @@ void CSVDoc::View::setupFileMenu() QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); connect (save, SIGNAL (triggered()), this, SLOT (save())); mSave = save; + + file->addSeparator(); QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); connect (verify, SIGNAL (triggered()), this, SLOT (verify())); @@ -80,6 +82,8 @@ void CSVDoc::View::setupFileMenu() QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); connect (meta, SIGNAL (triggered()), this, SLOT (addMetaDataSubView())); + file->addSeparator(); + QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); connect (close, SIGNAL (triggered()), this, SLOT (close())); @@ -156,17 +160,16 @@ void CSVDoc::View::setupWorldMenu() { QMenu *world = menuBar()->addMenu (tr ("World")); - QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); - connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); - - QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); - connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); - QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); connect (referenceables, SIGNAL (triggered()), this, SLOT (addReferenceablesSubView())); QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); + + world->addSeparator(); + + QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); + connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); QAction *lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); @@ -177,7 +180,10 @@ void CSVDoc::View::setupWorldMenu() QAction *grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); - world->addSeparator(); // items that don't represent single record lists follow here + world->addSeparator(); + + QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); + connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); QAction *regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); @@ -187,14 +193,19 @@ void CSVDoc::View::setupMechanicsMenu() { QMenu *mechanics = menuBar()->addMenu (tr ("Mechanics")); + QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); + connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); + connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); + QAction* globals = createMenuEntry(CSMWorld::UniversalId::Type_Globals, mechanics, "document-mechanics-globals"); connect (globals, SIGNAL (triggered()), this, SLOT (addGlobalsSubView())); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); connect (gmsts, SIGNAL (triggered()), this, SLOT (addGmstsSubView())); - - QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); - connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + mechanics->addSeparator(); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); @@ -204,9 +215,6 @@ void CSVDoc::View::setupMechanicsMenu() QAction* magicEffects = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); connect (magicEffects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); - - QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); - connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); } void CSVDoc::View::setupCharacterMenu() @@ -227,21 +235,25 @@ void CSVDoc::View::setupCharacterMenu() QAction* birthsigns = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); connect (birthsigns, SIGNAL (triggered()), this, SLOT (addBirthsignsSubView())); + + QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); + connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); + characters->addSeparator(); + QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); connect (topics, SIGNAL (triggered()), this, SLOT (addTopicsSubView())); + QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); + connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); + + characters->addSeparator(); + QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); - QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); - connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); - QAction* journalInfos = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); - - QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); - connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); } void CSVDoc::View::setupAssetsMenu() diff --git a/apps/opencs/view/render/brushdraw.cpp b/apps/opencs/view/render/brushdraw.cpp index 255a13a12e..6b33e336ea 100644 --- a/apps/opencs/view/render/brushdraw.cpp +++ b/apps/opencs/view/render/brushdraw.cpp @@ -18,6 +18,8 @@ CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textur mBrushDrawNode = new osg::Group(); mGeometry = new osg::Geometry(); mBrushDrawNode->addChild(mGeometry); + mBrushDrawNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mBrushDrawNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mParentNode->addChild(mBrushDrawNode); if (mTextureMode) mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_TEXTURE_SIZE); @@ -122,7 +124,14 @@ void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::V const float brushOutlineHeight (1.0f); float diameter = radius * 2; int resolution = static_cast(2.f * diameter / mLandSizeFactor); //half a vertex resolution - float resAdjustedLandSizeFactor = mLandSizeFactor / 2; + float resAdjustedLandSizeFactor = mLandSizeFactor / 2; //128 + + if (resolution > 128) // limit accuracy for performance + { + resolution = 128; + resAdjustedLandSizeFactor = diameter / resolution; + } + osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < resolution; i++) @@ -215,7 +224,8 @@ void CSVRender::BrushDraw::buildCircleGeometry(const float& radius, const osg::V osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); - const int amountOfPoints = (osg::PI * 2.0f) * radius / 20; + + const int amountOfPoints = 128; const float step ((osg::PI * 2.0f) / static_cast(amountOfPoints)); const float brushOutlineHeight (1.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index a654171fce..776dbf7890 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -60,7 +60,7 @@ void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; - const int offset = cellSize / 2 + 800; + const int offset = cellSize / 2 + 600; int x = mCoordinates.getX()*cellSize + cellSize/2; int y = mCoordinates.getY()*cellSize + cellSize/2; @@ -92,9 +92,9 @@ void CSVRender::CellArrow::buildShape() { osg::ref_ptr geometry (new osg::Geometry); - const int arrowWidth = 4000; - const int arrowLength = 1500; - const int arrowHeight = 500; + const int arrowWidth = 2700; + const int arrowLength = 1350; + const int arrowHeight = 300; osg::Vec3Array *vertices = new osg::Vec3Array; for (int i2=0; i2<2; ++i2) diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index d0e4dbe04a..789fad0587 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -682,18 +682,20 @@ void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) int cellColumn = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_Cell)); - int refNumColumn = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_RefNum)); + int origCellColumn = collection.findColumnIndex(static_cast ( + CSMWorld::Columns::ColumnId_OriginalCell)); if (cellIndex != originalIndex) { /// \todo figure out worldspace (not important until multiple worldspaces are supported) + std::string origCellId = CSMWorld::CellCoordinates(originalIndex).getId(""); std::string cellId = CSMWorld::CellCoordinates (cellIndex).getId (""); commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, cellColumn), QString::fromUtf8 (cellId.c_str()))); - commands.push (new CSMWorld::ModifyCommand( *model, - model->index (recordIndex, refNumColumn), 0)); + model->index (recordIndex, origCellColumn), QString::fromUtf8 (origCellId.c_str()))); + commands.push(new CSMWorld::ModifyCommand(*model, + model->index(recordIndex, cellColumn), QString::fromUtf8(cellId.c_str()))); + // NOTE: refnum is not modified for moving a reference to another cell } } diff --git a/apps/opencs/view/tools/merge.hpp b/apps/opencs/view/tools/merge.hpp index d394a431ed..c7c4585979 100644 --- a/apps/opencs/view/tools/merge.hpp +++ b/apps/opencs/view/tools/merge.hpp @@ -1,5 +1,5 @@ -#ifndef CSV_TOOLS_REPORTTABLE_H -#define CSV_TOOLS_REPORTTABLE_H +#ifndef CSV_TOOLS_MERGE_H +#define CSV_TOOLS_MERGE_H #include diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index f2360b1378..152472f504 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -538,6 +538,9 @@ void CSVWorld::EditWidget::remake(int row) mainLayout->addLayout(tablesLayout, QSizePolicy::Preferred); mainLayout->addStretch(1); + int blockedColumn = mTable->searchColumnIndex(CSMWorld::Columns::ColumnId_Blocked); + bool isBlocked = mTable->data(mTable->index(row, blockedColumn)).toInt(); + int unlocked = 0; int locked = 0; const int columns = mTable->columnCount(); @@ -583,6 +586,8 @@ void CSVWorld::EditWidget::remake(int row) NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); table->resizeColumnsToContents(); + if (isBlocked) + table->setEditTriggers(QAbstractItemView::NoEditTriggers); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); @@ -617,7 +622,9 @@ void CSVWorld::EditWidget::remake(int row) label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - if (! (mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable)) + // HACK: the blocked checkbox needs to keep the same position + // FIXME: unfortunately blocked record displays a little differently to unblocked one + if (!(mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable) || i == blockedColumn) { lockedLayout->addWidget (label, locked, 0); lockedLayout->addWidget (editor, locked, 1); @@ -639,7 +646,7 @@ void CSVWorld::EditWidget::remake(int row) createEditorContextMenu(editor, display, row); } } - else + else // Flag_Dialogue_List { CSMWorld::IdTree *tree = static_cast(mTable); mNestedTableMapper = new QDataWidgetMapper (this); @@ -686,7 +693,10 @@ void CSVWorld::EditWidget::remake(int row) label->setEnabled(false); } - createEditorContextMenu(editor, display, row); + if (!isBlocked) + createEditorContextMenu(editor, display, row); + else + editor->setEnabled(false); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 65ded46c7f..2681a398b2 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -78,6 +78,8 @@ QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptio for (std::vector >::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) comboBox->addItem (iter->second); + + comboBox->setMaxVisibleItems(20); return comboBox; } diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 1a2f2bbaa3..6bc0126b3e 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -22,7 +22,8 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); mType = new QComboBox (this); - + mType->setMaxVisibleItems(20); + for (std::vector::const_iterator iter (types.begin()); iter!=types.end(); ++iter) { @@ -31,7 +32,9 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd mType->addItem (QIcon (id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast (id2.getType())); } - + + mType->model()->sort(0); + insertBeforeButtons (mType, false); connect (mType, SIGNAL (currentIndexChanged (int)), this, SLOT (setType (int))); diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index aabb7ca2a7..53cd54e7ac 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -32,7 +32,6 @@ namespace CSVWidget namespace CSVWorld { - class Table; class TableBottomBox; class CreatorFactoryBase; diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 2834159b7f..643396a057 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -28,6 +28,7 @@ #include "../../model/prefs/shortcut.hpp" #include "tableeditidaction.hpp" +#include "tableheadermouseeventhandler.hpp" #include "util.hpp" void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) @@ -422,6 +423,8 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["ID Tables"].update(); + + new TableHeaderMouseEventHandler(this); } void CSVWorld::Table::setEditLock (bool locked) diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp new file mode 100644 index 0000000000..866c6149db --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -0,0 +1,64 @@ +#include "tableheadermouseeventhandler.hpp" +#include "dragrecordtable.hpp" + +#include +#include + +namespace CSVWorld +{ + +TableHeaderMouseEventHandler::TableHeaderMouseEventHandler(DragRecordTable * parent) + : QWidget(parent) + , table(*parent) + , header(*table.horizontalHeader()) +{ + header.setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect( + &header, &QHeaderView::customContextMenuRequested, [=](const QPoint & position) { showContextMenu(position); }); + + header.viewport()->installEventFilter(this); +} + +bool TableHeaderMouseEventHandler::eventFilter(QObject * tableWatched, QEvent * event) +{ + if (event->type() == QEvent::Type::MouseButtonPress) + { + auto & clickEvent = static_cast(*event); + if ((clickEvent.button() == Qt::MiddleButton)) + { + const auto & index = table.indexAt(clickEvent.pos()); + table.setColumnHidden(index.column(), true); + clickEvent.accept(); + return true; + } + } + return false; +} + +void TableHeaderMouseEventHandler::showContextMenu(const QPoint & position) +{ + auto & menu{createContextMenu()}; + menu.popup(header.viewport()->mapToGlobal(position)); +} + +QMenu & TableHeaderMouseEventHandler::createContextMenu() +{ + auto * menu = new QMenu(this); + for (int i = 0; i < table.model()->columnCount(); ++i) + { + const auto & name = table.model()->headerData(i, Qt::Horizontal, Qt::DisplayRole); + QAction * action{new QAction(name.toString(), this)}; + action->setCheckable(true); + action->setChecked(!table.isColumnHidden(i)); + menu->addAction(action); + + connect(action, &QAction::triggered, [=]() { + table.setColumnHidden(i, !action->isChecked()); + action->setChecked(!action->isChecked()); + action->toggle(); + }); + } + return *menu; +} + +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.hpp b/apps/opencs/view/world/tableheadermouseeventhandler.hpp new file mode 100644 index 0000000000..934bc1dbb7 --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace CSVWorld +{ +class DragRecordTable; + +class TableHeaderMouseEventHandler : public QWidget +{ +public: + explicit TableHeaderMouseEventHandler(DragRecordTable * parent); + + void showContextMenu(const QPoint &); + +private: + DragRecordTable & table; + QHeaderView & header; + + QMenu & createContextMenu(); + bool eventFilter(QObject *, QEvent *) override; + +}; // class TableHeaderMouseEventHandler +} // namespace CSVWorld diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 5605ff229e..7334d893db 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -2,6 +2,7 @@ set(GAME main.cpp engine.cpp + options.cpp ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest @@ -58,7 +59,7 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp actions object worldview userdataserializer eventqueue query luabindings localscripts objectbindings cellbindings asyncbindings settingsbindings - camerabindings uibindings inputbindings + camerabindings uibindings inputbindings nearbybindings ) add_openmw_dir (mwsound @@ -92,8 +93,8 @@ add_openmw_dir (mwmechanics drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects - spellabsorption linkedeffects + character actors objects aistate trading weaponpriority spellpriority weapontype spellutil + spellabsorption spelleffects ) add_openmw_dir (mwstate diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 772af3759e..bbe4f25f69 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -875,7 +875,14 @@ public: void join() { if (mThread) + { + { + std::lock_guard lk(mMutex); + mJoinRequest = true; + } + mCV.notify_one(); mThread->join(); + } } private: @@ -891,10 +898,12 @@ private: void threadBody() { - while (!mEngine->mViewer->done() && !mEngine->mEnvironment.getStateManager()->hasQuitRequest()) + while (true) { std::unique_lock lk(mMutex); - mCV.wait(lk, [&]{ return mUpdateRequest; }); + mCV.wait(lk, [&]{ return mUpdateRequest || mJoinRequest; }); + if (mJoinRequest) + break; update(); @@ -908,6 +917,7 @@ private: std::mutex mMutex; std::condition_variable mCV; bool mUpdateRequest = false; + bool mJoinRequest = false; double mDt = 0; bool mIsGuiMode = false; std::optional mThread; diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 5de177a907..18b1ab267f 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -7,6 +7,7 @@ #include #include "engine.hpp" +#include "options.hpp" #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN @@ -39,108 +40,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat namespace bpo = boost::program_options; typedef std::vector StringsVector; - bpo::options_description desc("Syntax: openmw \nAllowed options"); - - desc.add_options() - ("help", "print help message") - ("version", "print version information and quit") - - ("replace", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended") - - ("data", bpo::value()->default_value(Files::PathContainer(), "data") - ->multitoken()->composing(), "set data directories (later directories have higher priority)") - - ("data-local", bpo::value()->default_value(Files::PathContainer::value_type(), ""), - "set local data directory (highest priority)") - - ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") - ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") - - ("resources", bpo::value()->default_value(boost::filesystem::path(), "resources"), - "set resources directory") - - ("start", bpo::value()->default_value(""), - "set initial cell") - - ("content", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") - - ("groundcover", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") - - ("lua-scripts", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") - - ("no-sound", bpo::value()->implicit_value(true) - ->default_value(false), "disable all sounds") - - ("script-all", bpo::value()->implicit_value(true) - ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") - - ("script-all-dialogue", bpo::value()->implicit_value(true) - ->default_value(false), "compile all dialogue scripts at startup") - - ("script-console", bpo::value()->implicit_value(true) - ->default_value(false), "enable console-only script functionality") - - ("script-run", bpo::value()->default_value(""), - "select a file containing a list of console commands that is executed on startup") - - ("script-warn", bpo::value()->implicit_value (1) - ->default_value (1), - "handling of warnings when compiling scripts\n" - "\t0 - ignore warning\n" - "\t1 - show warning but consider script as correctly compiled anyway\n" - "\t2 - treat warnings as errors") - - ("script-blacklist", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") - - ("script-blacklist-use", bpo::value()->implicit_value(true) - ->default_value(true), "enable script blacklisting") - - ("load-savegame", bpo::value()->default_value(boost::filesystem::path(), ""), - "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") - - ("skip-menu", bpo::value()->implicit_value(true) - ->default_value(false), "skip main menu on game startup") - - ("new-game", bpo::value()->implicit_value(true) - ->default_value(false), "run new game sequence (ignored if skip-menu=0)") - - ("fs-strict", bpo::value()->implicit_value(true) - ->default_value(false), "strict file system handling (no case folding)") - - ("encoding", bpo::value()-> - default_value("win1252"), - "Character encoding used in OpenMW game messages:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - - ("fallback", bpo::value()->default_value(FallbackMap(), "") - ->multitoken()->composing(), "fallback values") - - ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") - - ("export-fonts", bpo::value()->implicit_value(true) - ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") - - ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") - - ("random-seed", bpo::value () - ->default_value(Misc::Rng::generateDefaultSeed()), - "seed value for random number generator") - ; - - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) - .options(desc).allow_unregistered().run(); + bpo::options_description desc = OpenMW::makeOptionsDescription(); bpo::variables_map variables; - // Runtime options override settings from all configs - bpo::store(valid_opts, variables); + Files::parseArgs(argc, argv, variables, desc); bpo::notify(variables); if (variables.count ("help")) @@ -158,9 +62,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat return false; } - bpo::variables_map composingVariables = cfgMgr.separateComposingVariables(variables, desc); + bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); cfgMgr.readConfiguration(variables, desc); - cfgMgr.mergeComposingVariables(variables, composingVariables, desc); + Files::mergeComposingVariables(variables, composingVariables, desc); Version::Version v = Version::getOpenmwVersion(variables["resources"].as().string()); Log(Debug::Info) << v.describe(); diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b638ecade9..6bedbb5b4d 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -58,7 +58,7 @@ namespace MWBase virtual void add (const MWWorld::Ptr& ptr) = 0; ///< Register an object for management - virtual void remove (const MWWorld::Ptr& ptr) = 0; + virtual void remove (const MWWorld::Ptr& ptr, bool keepActive) = 0; ///< Deregister an object for management virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; @@ -277,8 +277,6 @@ namespace MWBase virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; - - virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 1dbb2b96f6..8b33095fde 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -286,7 +286,7 @@ namespace MWBase virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes - virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true) = 0; + virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; @@ -539,7 +539,7 @@ namespace MWBase virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; - virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) = 0; virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; virtual void updateProjectilesCasters() = 0; @@ -591,7 +591,7 @@ namespace MWBase virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, - const std::string& sourceName, const bool fromProjectile=false) = 0; + const std::string& sourceName, const bool fromProjectile=false, int slot = 0) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; @@ -653,7 +653,8 @@ namespace MWBase virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; + virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors = nullptr) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index e4ba7af4f0..6285bdbf7e 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -38,15 +38,15 @@ namespace MWClass } } - void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 32bf1564c9..48a679e0b7 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -17,9 +17,9 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index b142fd2643..ad43bd6e5f 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -22,7 +22,7 @@ namespace MWClass MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if (!model.empty()) { diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 6a5d4c5150..886ffe4771 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -24,7 +24,7 @@ namespace MWClass ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 051fbafeb5..06980f55da 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -106,15 +106,15 @@ namespace MWClass } } - void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Container::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 9407dab243..0b290a73e1 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -42,8 +42,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 50dafba39b..54623e6699 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -846,6 +846,7 @@ namespace MWClass // Reset to original position MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 70c4a27582..b5fe705ca6 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -55,9 +55,9 @@ namespace MWClass } } - void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) @@ -70,10 +70,10 @@ namespace MWClass } } - void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door); } bool Door::isDoor() const diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 1fd59f8f52..f9288a88ce 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -18,8 +18,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool isDoor() const override; diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 0281861980..69cc1a09bf 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -33,13 +33,13 @@ namespace MWClass renderingInterface.getObjects().insertModel(ptr, model, true, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } - void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { MWWorld::LiveCellRef *ref = ptr.get(); assert (ref->mBase != nullptr); - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, @@ -47,11 +47,11 @@ namespace MWClass MWSound::PlayMode::Loop); } - void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects if (!model.empty() && (ptr.get()->mBase->mData.mFlags & ESM::Light::Carry) == 0) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } bool Light::useAnim() const diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 206abede43..e8aa4e5878 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -14,8 +14,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 41cf3beed5..718b4d972d 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1397,6 +1397,7 @@ namespace MWClass // Reset to original position MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 5cbe219580..0805ca3dd1 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -23,15 +23,15 @@ namespace MWClass } } - void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Static::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index 7c326987a8..c747eebf2f 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -14,8 +14,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index f296f223fb..0c14ea8f8d 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,8 +1,8 @@ #ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H -#include #include +#include #include #include #include // std::reverse @@ -68,6 +68,7 @@ public: return false; } + static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; @@ -78,16 +79,6 @@ public: std::vector matches; for (Point i = beg; i != end; ++i) { - // check if previous character marked start of new word - if (i != beg) - { - Point prev = i; - --prev; - if(isalpha(*prev)) - continue; - } - - // check first character typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i)); diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 827f87c7d6..43da1ef83f 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -72,13 +72,6 @@ namespace return {question, {r2, r1, r0}, sound}; } } - - void updatePlayerHealth() - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); - npcStats.updateHealth(); - } } namespace MWGui @@ -372,8 +365,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); mPickClassDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) @@ -448,8 +439,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); mRaceDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() @@ -477,8 +466,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); mBirthSignDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) @@ -527,7 +514,6 @@ namespace MWGui // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } - updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) @@ -719,8 +705,6 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); mPlayerClass = *klass; - - updatePlayerHealth(); } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index b649e41b0f..de771051ef 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -262,7 +262,7 @@ namespace MWGui } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); @@ -277,8 +277,9 @@ namespace MWGui auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); if(it != summons.end()) { - MWMechanics::purgeSummonEffect(summoner, *it); + auto summon = *it; summons.erase(it); + MWMechanics::purgeSummonEffect(summoner, summon); break; } } diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index cc793073e3..a029fe54b6 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -82,10 +82,7 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison - for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) - { - spell.second.mNextWorsening += mDays * 24; - } + player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); std::set skills; for (int day=0; day #include +#include #include #include #include @@ -66,35 +67,15 @@ namespace MWGui void LoadingScreen::findSplashScreens() { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string pattern = "Splash/"; - mResourceSystem->getVFS()->normalizeFilename(pattern); + auto isSupportedExtension = [](const std::string_view& ext) { + static const std::array supported_extensions{ {"tga", "dds", "ktx", "png", "bmp", "jpeg", "jpg"} }; + return !ext.empty() && std::find(supported_extensions.begin(), supported_extensions.end(), ext) != supported_extensions.end(); + }; - /* priority given to the left */ - const std::array supported_extensions {{".tga", ".dds", ".ktx", ".png", ".bmp", ".jpeg", ".jpg"}}; - - auto found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator("Splash/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos) - { - for(auto const& extension: supported_extensions) - { - if (name.compare(pos, name.size() - pos, extension) == 0) - { - mSplashScreens.push_back(found->first); - break; /* based on priority */ - } - } - } - } - else - break; - ++found; + if (isSupportedExtension(Misc::getFileExtension(name))) + mSplashScreens.push_back(name); } if (mSplashScreens.empty()) Log(Debug::Warning) << "Warning: no splash screens found!"; diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index eb51f560be..49870a9ddf 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -99,10 +99,8 @@ namespace MWGui std::vector spellsToSort; - for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) + for (const ESM::Spell* spell : merchantSpells) { - const ESM::Spell* spell = iter->first; - if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers @@ -115,10 +113,10 @@ namespace MWGui continue; } - if (playerHasSpell(iter->first->mId)) + if (playerHasSpell(spell->mId)) continue; - spellsToSort.push_back(iter->first); + spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 01653d9e6f..8c0ea865a8 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -520,10 +520,8 @@ namespace MWGui std::vector knownEffects; - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; - // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 405abfbae7..0673446fe7 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -24,50 +24,33 @@ namespace MWGui { - - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime, float totalTime) - { - MagicEffectInfo newEffectSource; - newEffectSource.mKey = key; - newEffectSource.mMagnitude = static_cast(magnitude); - newEffectSource.mPermanent = mIsPermanent; - newEffectSource.mRemainingTime = remainingTime; - newEffectSource.mSource = sourceName; - newEffectSource.mTotalTime = totalTime; - - mEffectSources[key.mId].push_back(newEffectSource); - } - - void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) { - // TODO: Tracking add/remove/expire would be better than force updating every frame - MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - - EffectSourceVisitor visitor; - - // permanent item enchantments & permanent spells - visitor.mIsPermanent = true; - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - store.visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - - // now add lasting effects - visitor.mIsPermanent = false; - stats.getActiveSpells().visitEffectSources(visitor); - - std::map >& effects = visitor.mEffectSources; + std::map> effects; + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + MagicEffectInfo newEffectSource; + newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg); + newEffectSource.mMagnitude = static_cast(effect.mMagnitude); + newEffectSource.mPermanent = effect.mDuration == -1.f; + newEffectSource.mRemainingTime = effect.mTimeLeft; + newEffectSource.mSource = params.getDisplayName(); + newEffectSource.mTotalTime = effect.mDuration; + effects[effect.mEffectId].push_back(newEffectSource); + } + } int w=2; - for (auto& effectInfoPair : effects) + for (const auto& [effectId, effectInfos] : effects) { - const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); @@ -78,7 +61,6 @@ namespace MWGui static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); - std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index b6aa49e69e..9825162a33 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -37,20 +37,6 @@ namespace MWGui bool mPermanent; // the effect is permanent }; - class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor - { - public: - bool mIsPermanent; - - std::map > mEffectSources; - - virtual ~EffectSourceVisitor() {} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - }; - class SpellIcons { public: diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index fe18b0f082..61ea9ce93a 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -92,9 +92,8 @@ namespace MWGui std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 22e3a55566..3fc0673735 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -74,7 +74,7 @@ namespace MWGui // Add price for the travelling followers std::set followers; - MWWorld::ActionTeleport::getFollowers(player, followers); + MWWorld::ActionTeleport::getFollowers(player, followers, !interior); // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 7a91f8643b..fa10ce03cd 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -99,8 +99,8 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - float xMove = xAxis * dt * 1500.0f / uiScale; - float yMove = yAxis * dt * 1500.0f / uiScale; + float xMove = xAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; + float yMove = yAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index 1f75760f7c..aad70d1a57 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -51,8 +51,8 @@ namespace MWLua std::array usedSlots; std::fill(usedSlots.begin(), usedSlots.end(), false); - constexpr int anySlot = -1; - auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView, anySlot](int slot, const Item& item) -> bool + static constexpr int anySlot = -1; + auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView](int slot, const Item& item) -> bool { auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); MWWorld::Ptr itemPtr; diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index fee6788b89..9fdda53d9d 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -1,5 +1,7 @@ #include "luabindings.hpp" +#include "luamanagerimp.hpp" + namespace sol { template <> @@ -48,11 +50,16 @@ namespace MWLua asyncId.mContainer->setupUnsavableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); }; + api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) + { + return Callback{std::move(fn), asyncId.mHiddenData}; + }; auto initializer = [](sol::table hiddenData) { LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; - return AsyncPackageId{id.mContainer, id.mPath}; + hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); + return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp index 68f2331b9e..46bf079d65 100644 --- a/apps/openmw/mwlua/camerabindings.cpp +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -7,7 +7,7 @@ namespace MWLua { sol::table api(context.mLua->sol(), sol::create); // TODO - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index f815d9c344..aaa00f3da9 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -48,7 +48,7 @@ namespace MWLua api["getControlSwitch"] = [input](const std::string& key) { return input->getControlSwitch(key); }; api["setControlSwitch"] = [input](const std::string& key, bool v) { input->toggleControlSwitch(key, v); }; - api["ACTION"] = context.mLua->makeReadOnly(context.mLua->sol().create_table_with( + api["ACTION"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( "GameMenu", MWInput::A_GameMenu, "Screenshot", MWInput::A_Screenshot, "Inventory", MWInput::A_Inventory, @@ -102,7 +102,7 @@ namespace MWLua "ZoomOut", MWInput::A_ZoomOut )); - api["CONTROL_SWITCH"] = context.mLua->makeReadOnly(context.mLua->sol().create_table_with( + api["CONTROL_SWITCH"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( "Controls", "playercontrols", "Fighting", "playerfighting", "Jumping", "playerjumping", @@ -112,7 +112,7 @@ namespace MWLua "VanityMode", "vanitymode" )); - api["CONTROLLER_BUTTON"] = context.mLua->makeReadOnly(context.mLua->sol().create_table_with( + api["CONTROLLER_BUTTON"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( "A", SDL_CONTROLLER_BUTTON_A, "B", SDL_CONTROLLER_BUTTON_B, "X", SDL_CONTROLLER_BUTTON_X, @@ -130,7 +130,7 @@ namespace MWLua "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT )); - api["CONTROLLER_AXIS"] = context.mLua->makeReadOnly(context.mLua->sol().create_table_with( + api["CONTROLLER_AXIS"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( "LeftX", SDL_CONTROLLER_AXIS_LEFTX, "LeftY", SDL_CONTROLLER_AXIS_LEFTY, "RightX", SDL_CONTROLLER_AXIS_RIGHTX, @@ -144,7 +144,7 @@ namespace MWLua "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveLeftRight )); - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index ed788eb525..aceffc24db 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -18,14 +18,14 @@ namespace MWLua sol::table res(lua.sol(), sol::create); for (const std::string& v : values) res[v] = v; - return lua.makeReadOnly(res); + return LuaUtil::makeReadOnly(res); } sol::table initCorePackage(const Context& context) { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 5; + api["API_REVISION"] = 7; api["quit"] = [lua]() { std::string traceback = lua->sol()["debug"]["traceback"]().get(); @@ -43,7 +43,7 @@ namespace MWLua "Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient", "Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon" }); - api["EQUIPMENT_SLOT"] = lua->makeReadOnly(lua->sol().create_table_with( + api["EQUIPMENT_SLOT"] = LuaUtil::makeReadOnly(lua->sol().create_table_with( "Helmet", MWWorld::InventoryStore::Slot_Helmet, "Cuirass", MWWorld::InventoryStore::Slot_Cuirass, "Greaves", MWWorld::InventoryStore::Slot_Greaves, @@ -64,7 +64,7 @@ namespace MWLua "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft, "Ammunition", MWWorld::InventoryStore::Slot_Ammunition )); - return lua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } sol::table initWorldPackage(const Context& context) @@ -107,37 +107,7 @@ namespace MWLua // return GObjectList{worldView->selectObjects(query, false)}; }; // TODO: add world.placeNewObject(recordId, cell, pos, [rot]) - return context.mLua->makeReadOnly(api); - } - - sol::table initNearbyPackage(const Context& context) - { - sol::table api(context.mLua->sol(), sol::create); - WorldView* worldView = context.mWorldView; - api["activators"] = LObjectList{worldView->getActivatorsInScene()}; - api["actors"] = LObjectList{worldView->getActorsInScene()}; - api["containers"] = LObjectList{worldView->getContainersInScene()}; - api["doors"] = LObjectList{worldView->getDoorsInScene()}; - api["items"] = LObjectList{worldView->getItemsInScene()}; - api["selectObjects"] = [context](const Queries::Query& query) - { - ObjectIdList list; - WorldView* worldView = context.mWorldView; - if (query.mQueryType == "activators") - list = worldView->getActivatorsInScene(); - else if (query.mQueryType == "actors") - list = worldView->getActorsInScene(); - else if (query.mQueryType == "containers") - list = worldView->getContainersInScene(); - else if (query.mQueryType == "doors") - list = worldView->getDoorsInScene(); - else if (query.mQueryType == "items") - list = worldView->getItemsInScene(); - return LObjectList{selectObjectsFromList(query, list, context)}; - // TODO: Maybe use sqlite - // return LObjectList{worldView->selectObjects(query, true)}; - }; - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } sol::table initQueryPackage(const Context& context) @@ -148,7 +118,7 @@ namespace MWLua query[t] = Queries::Query(std::string(t)); for (const QueryFieldGroup& group : getBasicQueryFieldGroups()) query[group.mName] = initFieldGroup(context, group); - return query; // makeReadonly is applied by LuaState::addCommonPackage + return query; // makeReadOnly is applied by LuaState::addCommonPackage } sol::table initFieldGroup(const Context& context, const QueryFieldGroup& group) @@ -163,12 +133,12 @@ namespace MWLua { const std::string& name = field->path()[i]; if (subgroup[name] == sol::nil) - subgroup[name] = context.mLua->makeReadOnly(context.mLua->newTable()); - subgroup = context.mLua->getMutableFromReadOnly(subgroup[name]); + subgroup[name] = LuaUtil::makeReadOnly(context.mLua->newTable()); + subgroup = LuaUtil::getMutableFromReadOnly(subgroup[name]); } subgroup[field->path().back()] = field; } - return context.mLua->makeReadOnly(res); + return LuaUtil::makeReadOnly(res); } } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index c58b556a3d..d1c62e43e3 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -21,11 +21,13 @@ namespace MWLua sol::table initCorePackage(const Context&); sol::table initWorldPackage(const Context&); - sol::table initNearbyPackage(const Context&); sol::table initQueryPackage(const Context&); sol::table initFieldGroup(const Context&, const QueryFieldGroup&); + // Implemented in nearbybindings.cpp + sol::table initNearbyPackage(const Context&); + // Implemented in objectbindings.cpp void initObjectBindingsForLocalScripts(const Context&); void initObjectBindingsForGlobalScripts(const Context&); @@ -45,9 +47,9 @@ namespace MWLua // Implemented in asyncbindings.cpp struct AsyncPackageId { - // TODO: add ObjectId mLocalObject; LuaUtil::ScriptsContainer* mContainer; std::string mScript; + sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 1e009081ff..38055c99b7 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -74,6 +74,16 @@ namespace MWLua mInitialized = true; } + void Callback::operator()(sol::object arg) const + { + if (mHiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY] != sol::nil) + LuaUtil::call(mFunc, std::move(arg)); + else + { + Log(Debug::Debug) << "Ignored callback to removed script " << mHiddenData.get(SCRIPT_NAME_KEY); + } + } + void LuaManager::update(bool paused, float dt) { ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); @@ -126,6 +136,11 @@ namespace MWLua << ". Object not found or has no attached scripts"; } + // Run queued callbacks + for (CallbackWithData& c : mQueuedCallbacks) + c.mCallback(c.mArg); + mQueuedCallbacks.clear(); + // Engine handlers in local scripts PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); if (playerScripts) diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 8fdfb02701..91f48171f3 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -19,6 +19,19 @@ namespace MWLua { + // Wrapper for a single-argument Lua function. + // Holds information about the script the function belongs to. + // Needed to prevent callback calls if the script was removed. + struct Callback + { + static constexpr std::string_view SCRIPT_NAME_KEY = "name"; + + sol::function mFunc; + sol::table mHiddenData; + + void operator()(sol::object arg) const; + }; + class LuaManager : public MWBase::LuaManager { public: @@ -67,6 +80,18 @@ namespace MWLua // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. void reloadAllScripts() override; + // Used to call Lua callbacks from C++ + void queueCallback(Callback callback, sol::object arg) { mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); } + + // Wraps Lua callback into an std::function. + // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or + // any other Lua-related function is running. + template + std::function wrapLuaCallback(const Callback& c) + { + return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; + } + private: LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); @@ -100,6 +125,13 @@ namespace MWLua std::vector mInputEvents; std::vector mActorAddedEvents; + struct CallbackWithData + { + Callback mCallback; + sol::object mArg; + }; + std::vector mQueuedCallbacks; + struct LocalEngineEvent { ObjectId mDest; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp new file mode 100644 index 0000000000..011d0ae9f3 --- /dev/null +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -0,0 +1,120 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwphysics/raycasting.hpp" + +#include "worldview.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + sol::table initNearbyPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + + sol::usertype rayResult = + context.mLua->sol().new_usertype("RayCastingResult"); + rayResult["hit"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) { return r.mHit; }); + rayResult["hitPos"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHit) + return r.mHitPos; + else + return sol::nullopt; + }); + rayResult["hitNormal"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHit) + return r.mHitNormal; + else + return sol::nullopt; + }); + rayResult["hitObject"] = sol::readonly_property([worldView](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHitObject.isEmpty()) + return sol::nullopt; + else + return LObject(getId(r.mHitObject), worldView->getObjectRegistry()); + }); + + api["COLLISION_TYPE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( + "World", MWPhysics::CollisionType_World, + "Door", MWPhysics::CollisionType_Door, + "Actor", MWPhysics::CollisionType_Actor, + "HeightMap", MWPhysics::CollisionType_HeightMap, + "Projectile", MWPhysics::CollisionType_Projectile, + "Water", MWPhysics::CollisionType_Water, + "Default", MWPhysics::CollisionType_Default)); + + api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + MWWorld::Ptr ignore; + int collisionType = MWPhysics::CollisionType_Default; + float radius = 0; + if (options) + { + sol::optional ignoreObj = options->get>("ignore"); + if (ignoreObj) ignore = ignoreObj->ptr(); + collisionType = options->get>("collisionType").value_or(collisionType); + radius = options->get>("radius").value_or(0); + } + const MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + if (radius <= 0) + return rayCasting->castRay(from, to, ignore, std::vector(), collisionType); + else + { + if (!ignore.isEmpty()) throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + return rayCasting->castSphere(from, to, radius, collisionType); + } + }; + // TODO: async raycasting + /*api["asyncCastRay"] = [luaManager = context.mLuaManager]( + const Callback& luaCallback, const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + std::function callback = + luaManager->wrapLuaCallback(luaCallback); + MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + + // Handle options the same way as in `castRay`. + + // NOTE: `callback` is not thread safe. If MWPhysics works in separate thread, it must put results to a queue + // and use this callback from the main thread at the beginning of the next frame processing. + rayCasting->asyncCastRay(callback, from, to, ignore, std::vector(), collisionType); + };*/ + + api["activators"] = LObjectList{worldView->getActivatorsInScene()}; + api["actors"] = LObjectList{worldView->getActorsInScene()}; + api["containers"] = LObjectList{worldView->getContainersInScene()}; + api["doors"] = LObjectList{worldView->getDoorsInScene()}; + api["items"] = LObjectList{worldView->getItemsInScene()}; + api["selectObjects"] = [context](const Queries::Query& query) + { + ObjectIdList list; + WorldView* worldView = context.mWorldView; + if (query.mQueryType == "activators") + list = worldView->getActivatorsInScene(); + else if (query.mQueryType == "actors") + list = worldView->getActorsInScene(); + else if (query.mQueryType == "containers") + list = worldView->getContainersInScene(); + else if (query.mQueryType == "doors") + list = worldView->getDoorsInScene(); + else if (query.mQueryType == "items") + list = worldView->getItemsInScene(); + return LObjectList{selectObjectsFromList(query, list, context)}; + // TODO: Maybe use sqlite + // return LObjectList{worldView->selectObjects(query, true)}; + }; + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/settingsbindings.cpp b/apps/openmw/mwlua/settingsbindings.cpp index 0e267182ac..12dd69f73a 100644 --- a/apps/openmw/mwlua/settingsbindings.cpp +++ b/apps/openmw/mwlua/settingsbindings.cpp @@ -62,7 +62,7 @@ namespace MWLua else return sol::make_object(lua->sol(), value.getFloat()); }; - return lua->makeReadOnly(config); + return LuaUtil::makeReadOnly(config); } sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, true, false); } diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index cb14c41621..4fae84cd40 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -12,7 +12,7 @@ namespace MWLua { luaManager->addUIMessage(message); }; - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 319d797257..6ad6ef369e 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -5,97 +5,279 @@ #include +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "spelleffects.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" + +namespace +{ + bool merge(std::vector& present, const std::vector& queued) + { + // Can't merge if we already have an effect with the same effect index + auto problem = std::find_if(queued.begin(), queued.end(), [&] (const auto& qEffect) + { + return std::find_if(present.begin(), present.end(), [&] (const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end(); + }); + if(problem != queued.end()) + return false; + present.insert(present.end(), queued.begin(), queued.end()); + return true; + } + + void addEffects(std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) + { + int currentEffectIndex = 0; + for(const auto& enam : list.mList) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mEffectIndex = currentEffectIndex++; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if(ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effects.emplace_back(effect); + } + } +} namespace MWMechanics { - void ActiveSpells::update(float duration) const + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { - bool rebuild = false; + mActiveSpells.mIterating = true; + } - // Erase no longer active spells and effects - if (duration > 0) + ActiveSpells::IterationGuard::~IterationGuard() + { + mActiveSpells.mIterating = false; + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) + : mId(cast.mId), mDisplayName(cast.mSourceName), mCasterActorId(-1), mSlot(cast.mSlot), mType(cast.mType), mWorsenings(-1) + { + if(!caster.isEmpty() && caster.getClass().isActor()) + mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId(); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) + : mId(spell->mId), mDisplayName(spell->mName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()), mSlot(0) + , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability : ESM::ActiveSpells::Type_Permanent), mWorsenings(-1) + { + assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + addEffects(mEffects, spell->mEffects, ignoreResistances); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor) + : mId(item.getCellRef().getRefId()), mDisplayName(item.getClass().getName(item)), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(slotIndex), mType(ESM::ActiveSpells::Type_Enchantment), mWorsenings(-1) + { + assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); + addEffects(mEffects, enchantment->mEffects); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) + : mId(params.mId), mEffects(params.mEffects), mDisplayName(params.mDisplayName), mCasterActorId(params.mCasterActorId) + , mSlot(params.mItem.isSet() ? params.mItem.mIndex : 0) + , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) + {} + + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const + { + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = mId; + params.mEffects = mEffects; + params.mDisplayName = mDisplayName; + params.mCasterActorId = mCasterActorId; + params.mItem.unset(); + if(mSlot) { - TContainer::iterator iter (mSpells.begin()); - while (iter!=mSpells.end()) + // Note that we're storing the inventory slot as a RefNum instead of an int as a matter of future proofing + // mSlot needs to be replaced with a RefNum once inventory items get persistent RefNum (#4508 #6148) + params.mItem = { static_cast(mSlot), 0 }; + } + params.mType = mType; + params.mWorsenings = mWorsenings; + params.mNextWorsening = mNextWorsening.toEsm(); + return params; + } + + void ActiveSpells::ActiveSpellParams::worsen() + { + ++mWorsenings; + if(!mWorsenings) + mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); + mNextWorsening += CorprusStats::sWorseningPeriod; + } + + bool ActiveSpells::ActiveSpellParams::shouldWorsen() const + { + return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; + } + + void ActiveSpells::ActiveSpellParams::resetWorsenings() + { + mWorsenings = -1; + } + + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) + { + const auto& creatureStats = ptr.getClass().getCreatureStats(ptr); + assert(&creatureStats.getActiveSpells() == this); + IterationGuard guard{*this}; + // Erase no longer active spells and effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) + { + if(spellIt->mType != ESM::ActiveSpells::Type_Temporary && spellIt->mType != ESM::ActiveSpells::Type_Consumable) { - if (!timeToExpire (iter)) + ++spellIt; + continue; + } + bool removedSpell = false; + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f) { - mSpells.erase (iter++); - rebuild = true; + auto effect = *effectIt; + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + removedSpell = applyPurges(ptr, &spellIt, &effectIt); + if(removedSpell) + break; } else { - bool interrupt = false; - std::vector& effects = iter->second.mEffects; - for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) + ++effectIt; + } + } + if(removedSpell) + continue; + if(spellIt->mEffects.empty()) + spellIt = mSpells.erase(spellIt); + else + ++spellIt; + } + + for(const auto& spell : mQueue) + addToSpells(ptr, spell); + mQueue.clear(); + + // Vanilla only does this on cell change I think + const auto& spells = creatureStats.getSpells(); + for(const ESM::Spell* spell : spells) + { + if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + mSpells.emplace_back(ActiveSpellParams{spell, ptr}); + } + + if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) + { + auto& store = ptr.getClass().getInventoryStore(ptr); + if(store.getInvListener() != nullptr) + { + bool playNonLooping = !store.isFirstEquip(); + const auto world = MWBase::Environment::get().getWorld(); + for(int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if(slot == store.end()) + continue; + const auto& enchantmentId = slot->getClass().getEnchantment(*slot); + if(enchantmentId.empty()) + continue; + const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); + if(enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + continue; + if(std::find_if(mSpells.begin(), mSpells.end(), [&] (const ActiveSpellParams& params) { - if (effectIt->mTimeLeft <= 0) - { - rebuild = true; - - // Note: it we expire a Corprus effect, we should remove the whole spell. - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - iter = mSpells.erase (iter); - interrupt = true; - break; - } - - effectIt = effects.erase(effectIt); - } - else - { - effectIt->mTimeLeft -= duration; - ++effectIt; - } - } - - if (!interrupt) - ++iter; + return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId(); + }) != mSpells.end()) + continue; + ActiveSpellParams params(*slot, enchantment, slotIndex, ptr); + mSpells.emplace_back(params); + for(const auto& effect : params.mEffects) + MWMechanics::playEffects(ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); } } } - if (mSpellsChanged) + // Update effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - mSpellsChanged = false; - rebuild = true; - } + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? + bool removedSpell = false; + for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) + { + bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(remove) + it = spellIt->mEffects.erase(it); + else + ++it; + removedSpell = applyPurges(ptr, &spellIt, &it); + if(removedSpell) + break; + } + if(removedSpell) + continue; - if (rebuild) - rebuildEffects(); + bool remove = false; + if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent) + remove = !spells.hasSpell(spellIt->mId); + else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment) + { + const auto& store = ptr.getClass().getInventoryStore(ptr); + auto slot = store.getSlot(spellIt->mSlot); + remove = slot == store.end() || slot->getCellRef().getRefId() != spellIt->mId; + } + if(remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + continue; + } + ++spellIt; + } } - void ActiveSpells::rebuildEffects() const + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - mEffects = MagicEffects(); - - for (TIterator iter (begin()); iter!=end(); ++iter) + if(spell.mType != ESM::ActiveSpells::Type_Consumable) { - const std::vector& effects = iter->second.mEffects; - - for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + auto found = std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& existing) { - if (effectIt->mTimeLeft > 0) - mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); + return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId && spell.mSlot == existing.mSlot; + }); + if(found != mSpells.end()) + { + if(merge(found->mEffects, spell.mEffects)) + return; + auto params = *found; + mSpells.erase(found); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); } } + mSpells.emplace_back(spell); } - ActiveSpells::ActiveSpells() - : mSpellsChanged (false) + ActiveSpells::ActiveSpells() : mIterating(false) {} - const MagicEffects& ActiveSpells::getMagicEffects() const - { - update(0.f); - return mEffects; - } - ActiveSpells::TIterator ActiveSpells::begin() const { return mSpells.begin(); @@ -106,246 +288,159 @@ namespace MWMechanics return mSpells.end(); } - double ActiveSpells::timeToExpire (const TIterator& iterator) const - { - const std::vector& effects = iterator->second.mEffects; - - float duration = 0; - - for (std::vector::const_iterator iter (effects.begin()); - iter!=effects.end(); ++iter) - { - if (iter->mTimeLeft > duration) - duration = iter->mTimeLeft; - } - - if (duration < 0) - return 0; - - return duration; - } - bool ActiveSpells::isSpellActive(const std::string& id) const { - for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) + return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell) { - if (Misc::StringUtils::ciEqual(iter->first, id)) - return true; - } - return false; + return Misc::StringUtils::ciEqual(spell.mId, id); + }) != mSpells.end(); } - const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const + void ActiveSpells::addSpell(const ActiveSpellParams& params) { - return mSpells; + mQueue.emplace_back(params); } - void ActiveSpells::addSpell(const std::string &id, bool stack, const std::vector& effects, - const std::string &displayName, int casterActorId) + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) { - TContainer::iterator it(mSpells.find(id)); - - ActiveSpellParams params; - params.mEffects = effects; - params.mDisplayName = displayName; - params.mCasterActorId = casterActorId; - - if (it == end() || stack) - { - mSpells.insert(std::make_pair(id, params)); - } - else - { - // addSpell() is called with effects for a range. - // but a spell may have effects with different ranges (e.g. Touch & Target) - // so, if we see new effects for same spell assume additional - // spell effects and add to existing effects of spell - mergeEffects(params.mEffects, it->second.mEffects); - it->second = params; - } - - mSpellsChanged = true; + mQueue.emplace_back(ActiveSpellParams{spell, actor, true}); } - void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) + void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) { - for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - // if effect is not in addTo, add it - bool missing = true; - for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + IterationGuard guard{*this}; + applyPurges(ptr); + } + } + + void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) + { + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) + { + IterationGuard guard{*this}; + applyPurges(ptr); + } + } + + bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, std::vector::iterator* currentEffect) + { + bool removedCurrentSpell = false; + while(!mPurges.empty()) + { + auto predicate = mPurges.front(); + mPurges.pop(); + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) + bool isCurrentSpell = currentSpell && *currentSpell == spellIt; + std::visit([&] (auto&& variant) { - missing = false; - break; - } - } - if (missing) - { - addTo.push_back(*effect); + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if(variant(*spellIt)) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + if(isCurrentSpell) + { + *currentSpell = spellIt; + removedCurrentSpell = true; + } + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + } + else + ++spellIt; + } + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(variant(*spellIt, *effectIt)) + { + auto effect = *effectIt; + if(isCurrentSpell && currentEffect) + { + auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect); + if(effectIt <= *currentEffect) + distance--; + effectIt = spellIt->mEffects.erase(effectIt); + *currentEffect = spellIt->mEffects.begin() + distance; + } + else + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + } + else + ++effectIt; + } + ++spellIt; + } + }, predicate); } } + return removedCurrentSpell; } - void ActiveSpells::removeEffects(const std::string &id) + void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const std::string &id) { - for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) + purge([=] (const ActiveSpellParams& params) { - if (spell->first == id) - { - spell->second.mEffects.clear(); - mSpellsChanged = true; - } - } + return params.mId == id; + }, ptr); } - void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const + void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId) { - for (TContainer::const_iterator it = begin(); it != end(); ++it) + purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect) { - for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end(); ++effectIt) - { - std::string name = it->second.mDisplayName; - - float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); - } - } + return effect.mEffectId == effectId; + }, ptr); } - void ActiveSpells::purgeAll(float chance, bool spellOnly) + void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) + purge([=] (const ActiveSpellParams& params) { - const std::string spellId = it->first; - - // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. - if (spellOnly) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) - { - ++it; - continue; - } - } - - if (Misc::Rng::roll0to99() < chance) - mSpells.erase(it++); - else - ++it; - } - mSpellsChanged = true; + return params.mCasterActorId == casterActorId; + }, ptr); } - void ActiveSpells::purgeEffect(short effectId) + void ActiveSpells::clear(const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + mQueue.clear(); + purge([] (const ActiveSpellParams& params) { return true; }, ptr); + } + + void ActiveSpells::skipWorsenings(double hours) + { + for(auto& spell : mSpells) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + if(spell.mWorsenings >= 0) + spell.mNextWorsening += hours; } - mSpellsChanged = true; - } - - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) - { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } - } - mSpellsChanged = true; - } - - void ActiveSpells::purge(int casterActorId) - { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (it->second.mCasterActorId == casterActorId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } - } - mSpellsChanged = true; - } - - void ActiveSpells::purgeCorprusDisease() - { - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) - { - bool hasCorprusEffect = false; - for (std::vector::iterator effectIt = iter->second.mEffects.begin(); - effectIt != iter->second.mEffects.end();++effectIt) - { - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } - - if (hasCorprusEffect) - { - mSpells.erase(iter++); - mSpellsChanged = true; - } - else - ++iter; - } - } - - void ActiveSpells::clear() - { - mSpells.clear(); - mSpellsChanged = true; } void ActiveSpells::writeState(ESM::ActiveSpells &state) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ESM::ActiveSpells::ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - state.mSpells.insert (std::make_pair(it->first, params)); - } + for(const auto& spell : mSpells) + state.mSpells.emplace_back(spell.toEsm()); + for(const auto& spell : mQueue) + state.mQueue.emplace_back(spell.toEsm()); } void ActiveSpells::readState(const ESM::ActiveSpells &state) { - for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) - { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - mSpells.insert (std::make_pair(it->first, params)); - mSpellsChanged = true; - } + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + mSpells.emplace_back(ActiveSpellParams{spell}); + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) + mQueue.emplace_back(ActiveSpellParams{spell}); } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 8bc29aa444..0538d7a8b7 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -1,40 +1,83 @@ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H -#include -#include +#include +#include +#include #include +#include +#include #include #include "../mwworld/timestamp.hpp" +#include "../mwworld/ptr.hpp" #include "magiceffects.hpp" +#include "spellcasting.hpp" + +namespace ESM +{ + struct Enchantment; + struct Spell; +} namespace MWMechanics { /// \brief Lasting spell effects /// - /// \note The name of this class is slightly misleading, since it also handels lasting potion + /// \note The name of this class is slightly misleading, since it also handles lasting potion /// effects. class ActiveSpells { public: - typedef ESM::ActiveEffect ActiveEffect; - - struct ActiveSpellParams + using ActiveEffect = ESM::ActiveEffect; + class ActiveSpellParams { - std::vector mEffects; - MWWorld::TimeStamp mTimeStamp; - std::string mDisplayName; + std::string mId; + std::vector mEffects; + std::string mDisplayName; + int mCasterActorId; + int mSlot; + ESM::ActiveSpells::EffectType mType; + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; - // The caster that inflicted this spell on us - int mCasterActorId; + ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params); + + ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false); + + ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + + ESM::ActiveSpells::ActiveSpellParams toEsm() const; + + friend class ActiveSpells; + public: + ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + + const std::string& getId() const { return mId; } + + const std::vector& getEffects() const { return mEffects; } + std::vector& getEffects() { return mEffects; } + + ESM::ActiveSpells::EffectType getType() const { return mType; } + + int getCasterActorId() const { return mCasterActorId; } + + int getWorsenings() const { return mWorsenings; } + + const std::string& getDisplayName() const { return mDisplayName; } + + // Increments worsenings count and sets the next timestamp + void worsen(); + + bool shouldWorsen() const; + + void resetWorsenings(); }; - typedef std::multimap TContainer; - typedef TContainer::const_iterator TIterator; + typedef std::list::const_iterator TIterator; void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; @@ -43,24 +86,29 @@ namespace MWMechanics TIterator end() const; - void update(float duration) const; + void update(const MWWorld::Ptr& ptr, float duration); private: + using ParamsPredicate = std::function; + using EffectPredicate = std::function; + using Predicate = std::variant; - mutable TContainer mSpells; - mutable MagicEffects mEffects; - mutable bool mSpellsChanged; + struct IterationGuard + { + ActiveSpells& mActiveSpells; - void rebuildEffects() const; + IterationGuard(ActiveSpells& spells); + ~IterationGuard(); + }; - /// Add any effects that are in "from" and not in "addTo" to "addTo" - void mergeEffects(std::vector& addTo, const std::vector& from); + std::list mSpells; + std::vector mQueue; + std::queue mPurges; + bool mIterating; - double timeToExpire (const TIterator& iterator) const; - ///< Returns time (in in-game hours) until the spell pointed to by \a iterator - /// expires. + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); - const TContainer& getActiveSpells() const; + bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); public: @@ -70,40 +118,31 @@ namespace MWMechanics /// /// \brief addSpell /// \param id ID for stacking purposes. - /// \param stack If false, the spell is not added if one with the same ID exists already. - /// \param effects - /// \param displayName Name for display in magic menu. /// - void addSpell (const std::string& id, bool stack, const std::vector& effects, - const std::string& displayName, int casterActorId); + void addSpell (const ActiveSpellParams& params); + + /// Bypasses resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects (const std::string& id); + void removeEffects (const MWWorld::Ptr& ptr, const std::string& id); /// Remove all active effects with this effect id - void purgeEffect (short effectId); + void purgeEffect (const MWWorld::Ptr& ptr, short effectId); - /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); + void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); + void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); - /// Remove all active effects, if roll succeeds (for each effect) - void purgeAll(float chance, bool spellOnly = false); - - /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId - void purge (int casterActorId); + /// Remove all effects that were cast by \a casterActorId + void purge (const MWWorld::Ptr& ptr, int casterActorId); /// Remove all spells - void clear(); + void clear(const MWWorld::Ptr& ptr); bool isSpellActive (const std::string& id) const; ///< case insensitive - void purgeCorprusDisease(); - - const MagicEffects& getMagicEffects() const; - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - + void skipWorsenings(double hours); }; } diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp index a5c55633ac..5801ea751d 100644 --- a/apps/openmw/mwmechanics/actor.cpp +++ b/apps/openmw/mwmechanics/actor.cpp @@ -5,6 +5,7 @@ namespace MWMechanics { Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) + : mPositionAdjusted(false) { mCharacterController.reset(new CharacterController(ptr, animation)); } @@ -58,4 +59,14 @@ namespace MWMechanics { mIsTurningToPlayer = turning; } + + void Actor::setPositionAdjusted(bool adjusted) + { + mPositionAdjusted = adjusted; + } + + bool Actor::getPositionAdjusted() const + { + return mPositionAdjusted; + } } diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index be4f425378..c25fc1cb73 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -48,6 +48,9 @@ namespace MWMechanics return mEngageCombat.update(duration); } + void setPositionAdjusted(bool adjusted); + bool getPositionAdjusted() const; + private: std::unique_ptr mCharacterController; int mGreetingTimer{0}; @@ -55,6 +58,7 @@ namespace MWMechanics GreetingState mGreetingState{Greet_None}; bool mIsTurningToPlayer{false}; Misc::DeviatingPeriodicTimer mEngageCombat{1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f)}; + bool mPositionAdjusted; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 37b438aefa..bde18aa584 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -44,7 +44,6 @@ #include "actor.hpp" #include "summoning.hpp" #include "actorutil.hpp" -#include "tickableeffects.hpp" namespace { @@ -55,67 +54,26 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -int getBoundItemSlot (const std::string& itemId) +bool isCommanded(const MWWorld::Ptr& actor) { - static std::map boundItemsMap; - if (boundItemsMap.empty()) + const auto& stats = actor.getClass().getCreatureStats(actor); + for(const auto& params : stats.getActiveSpells()) { - const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); - - std::string boundId = store.find("sMagicBoundBootsID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; - - boundId = store.find("sMagicBoundCuirassID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; - - boundId = store.find("sMagicBoundLeftGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; - - boundId = store.find("sMagicBoundRightGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; - - boundId = store.find("sMagicBoundHelmID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; - - boundId = store.find("sMagicBoundShieldID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; + for(const auto& effect : params.getEffects()) + { + if(((effect.mEffectId == ESM::MagicEffect::CommandHumanoid && actor.getClass().isNpc()) + || (effect.mEffectId == ESM::MagicEffect::CommandCreature && !actor.getClass().isNpc())) + && effect.mMagnitude >= stats.getLevel()) + return true; + } } - - int slot = MWWorld::InventoryStore::Slot_CarriedRight; - std::map::iterator it = boundItemsMap.find(itemId); - if (it != boundItemsMap.end()) - slot = it->second; - - return slot; + return false; } -class CheckActorCommanded : public MWMechanics::EffectSourceVisitor -{ - MWWorld::Ptr mActor; -public: - bool mCommanded; - - CheckActorCommanded(const MWWorld::Ptr& actor) - : mActor(actor) - , mCommanded(false){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) - || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) - && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) - mCommanded = true; - } -}; - // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { - CheckActorCommanded check(actor); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - stats.getActiveSpells().visitEffectSources(check); bool hasCommandPackage = false; @@ -130,7 +88,7 @@ void adjustCommandedActor (const MWWorld::Ptr& actor) } } - if (!check.mCommanded && hasCommandPackage) + if (!isCommanded(actor) && hasCommandPackage) stats.getAiSequence().erase(it); } @@ -169,129 +127,43 @@ void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWW } } -} - -namespace MWMechanics +float getStuntedMagickaDuration(const MWWorld::Ptr& actor) { - static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player - static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement - static const float DECELERATE_DISTANCE = 512.f; - - namespace + float remainingTime = 0.f; + for(const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { - float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) + for(const auto& effect : params.getEffects()) { - const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); - return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; - } - } - - class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor - { - public: - float mRemainingTime; - - GetStuntedMagickaDuration(const MWWorld::Ptr& actor) - : mRemainingTime(0.f){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mRemainingTime == -1) return; - - if (key.mId == ESM::MagicEffect::StuntedMagicka) + if(effect.mEffectId == ESM::MagicEffect::StuntedMagicka) { - if (totalTime == -1) - { - mRemainingTime = -1; - return; - } - - if (remainingTime > mRemainingTime) - mRemainingTime = remainingTime; + if(effect.mDuration == -1.f) + return -1.f; + remainingTime = std::max(remainingTime, effect.mTimeLeft); } } - }; + } + return remainingTime; +} - class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor +void soulTrap(const MWWorld::Ptr& creature) +{ + const auto& stats = creature.getClass().getCreatureStats(creature); + if(!stats.getMagicEffects().get(ESM::MagicEffect::Soultrap).getMagnitude()) + return; + int creatureSoulValue = creature.get()->mBase->mData.mSoul; + if (creatureSoulValue == 0) + return; + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); + for(const auto& params : stats.getActiveSpells()) { - std::string mSpellId; - - public: - GetCurrentMagnitudes(const std::string& spellId) - : mSpellId(spellId) + for(const auto& effect : params.getEffects()) { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (magnitude <= 0) - return; - - if (sourceId != mSpellId) - return; - - mMagnitudes.emplace_back(key, magnitude); - } - - std::vector> mMagnitudes; - }; - - class GetCorprusSpells : public MWMechanics::EffectSourceVisitor - { - - public: - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (key.mId != ESM::MagicEffect::Corprus) - return; - - mSpells.push_back(sourceId); - } - - std::vector mSpells; - }; - - class SoulTrap : public MWMechanics::EffectSourceVisitor - { - MWWorld::Ptr mCreature; - MWWorld::Ptr mActor; - bool mTrapped; - public: - SoulTrap(const MWWorld::Ptr& trappedCreature) - : mCreature(trappedCreature) - , mTrapped(false) - { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mTrapped) - return; - if (key.mId != ESM::MagicEffect::Soultrap) - return; - if (magnitude <= 0) - return; - - MWBase::World* world = MWBase::Environment::get().getWorld(); - - MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); + if(effect.mEffectId != ESM::MagicEffect::Soultrap || effect.mMagnitude <= 0.f) + continue; + MWWorld::Ptr caster = world->searchPtrViaActorId(params.getCasterActorId()); if (caster.isEmpty() || !caster.getClass().isActor()) - return; - - static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); - - int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; - if (creatureSoulValue == 0) - return; + continue; // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); @@ -316,126 +188,58 @@ namespace MWMechanics } if (gem == container.end()) - return; + continue; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem, caster); - gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); + gem->getCellRef().setSoul(creature.getCellRef().getRefId()); // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); - mTrapped = true; - - if (caster == getPlayer()) + if (caster == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + const ESM::Static* fx = world->getStore().get() .search("VFX_Soul_Trap"); if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", mCreature.getRefData().getPosition().asVec3()); + world->spawnEffect("meshes\\" + fx->mModel, "", creature.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getSoundManager()->playSound3D( - mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f - ); + MWBase::Environment::get().getSoundManager()->playSound3D(creature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f); + return; //remove to get vanilla behaviour } - }; - - void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; - - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); - - if (actor != MWMechanics::getPlayer()) - return; - - MWWorld::Ptr newItem; - auto it = store.getSlot(slot); - // Equip can fail because beast races cannot equip boots/helmets - if(it != store.end()) - newItem = *it; - - if (newItem.isEmpty() || boundPtr != newItem) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState_Weapon); - - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } +} - void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) +void removeTemporaryEffects(const MWWorld::Ptr& ptr) +{ + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purge([] (const auto& spell) { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); + return spell.getType() == ESM::ActiveSpells::Type_Consumable || spell.getType() == ESM::ActiveSpells::Type_Temporary; + }, ptr); +} +} - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); +namespace MWMechanics +{ + static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement + static const float DECELERATE_DISTANCE = 512.f; - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); - - if (actor != MWMechanics::getPlayer()) + namespace + { + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { - store.remove(itemId, 1, actor); - - // Equip a replacement - if (!wasEquipped) - return; - - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) - return; - - if (actor.getClass().getCreatureStats(actor).isDead()) - return; - - if (!actor.getClass().hasInventoryStore(actor)) - return; - - if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) - return; - - actor.getClass().getInventoryStore(actor).autoEquip(actor); - - return; + const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); + return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; } - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player.getPreviousItem(itemId); - player.erasePreviousItem(itemId); - - if (!prevItemId.empty()) - { - // Find previous item (or its replacement) by id. - // we should equip previous item only if expired bound item was equipped. - MWWorld::Ptr item = store.findReplacement(prevItemId); - if (!item.isEmpty() && wasEquipped) - { - MWWorld::ActionEquip action(item); - action.execute(actor); - } - } - - store.remove(itemId, 1, actor); } void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects - adjustMagicEffects (ptr); + adjustMagicEffects (ptr, duration); if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) calculateDynamicStats (ptr); @@ -793,23 +597,61 @@ namespace MWMechanics } } - void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) + void Actors::adjustMagicEffects (const MWWorld::Ptr& creature, float duration) { - CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); - if (creatureStats.isDeathAnimationFinished()) - return; + CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + bool wasDead = creatureStats.isDead(); - MagicEffects now = creatureStats.getSpells().getMagicEffects(); + creatureStats.getActiveSpells().update(creature, duration); - if (creature.getClass().hasInventoryStore(creature)) + if (!wasDead && creatureStats.isDead()) { - MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); - now += store.getMagicEffects(); + // The actor was killed by a magic effect. Figure out if the player was responsible for it. + const ActiveSpells& spells = creatureStats.getActiveSpells(); + MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + bool actorKilled = false; + + const ActiveSpells::ActiveSpellParams& spell = *it; + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); + for (const auto& effect : spell.getEffects()) + { + static const std::array damageEffects{ + ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, + ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth + }; + + bool isDamageEffect = std::find(damageEffects.begin(), damageEffects.end(), effect.mEffectId) != damageEffects.end(); + + if (isDamageEffect) + { + if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); + actorKilled = true; + break; + } + } + } + + if (actorKilled) + break; + } } - now += creatureStats.getActiveSpells().getMagicEffects(); + // updateSummons assumes the actor belongs to a cell. + // This assumption isn't always valid for the player character. + if (!creature.isInCell()) + return; - creatureStats.modifyMagicEffects(now); + if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) + updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) @@ -857,22 +699,18 @@ namespace MWMechanics if (stunted) { // Stunted Magicka effect should be taken into account. - GetStuntedMagickaDuration visitor(ptr); - stats.getActiveSpells().visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); + float remainingTime = getStuntedMagickaDuration(ptr); // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. - if (visitor.mRemainingTime > 0) + if (remainingTime > 0) { double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if(timeScale == 0.0) timeScale = 1; - restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); + restoreHours = std::max(0.0, hours - remainingTime * timeScale / 3600.f); } - else if (visitor.mRemainingTime == -1) + else if (remainingTime == -1) restoreHours = 0; } @@ -933,398 +771,12 @@ namespace MWMechanics stats.setFatigue (fatigue); } - class ExpiryVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - float mDuration; - - public: - ExpiryVisitor(const MWWorld::Ptr& actor, float duration) - : mActor(actor), mDuration(duration) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override - { - if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) - { - CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) - creatureStats.getMagicEffects().add(key, -magnitude); - } - } - }; - - void Actors::applyCureEffects(const MWWorld::Ptr& actor) - { - CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); - const MagicEffects &effects = creatureStats.getMagicEffects(); - - if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); - } - if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); - } - if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeCommonDisease(); - } - if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeBlightDisease(); - } - if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeCorprusDisease(); - creatureStats.getSpells().purgeCorprusDisease(); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); - } - if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) - { - creatureStats.getSpells().purgeCurses(); - } - } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) { CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - const MagicEffects &effects = creatureStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - applyCureEffects(ptr); - - bool wasDead = creatureStats.isDead(); - - if (duration > 0) - { - // Apply correct magnitude for tickable effects that have just expired, - // in case duration > remaining time of effect. - // One case where this will happen is when the player uses the rest/wait command - // while there is a tickable effect active that should expire before the end of the rest/wait. - ExpiryVisitor visitor(ptr, duration); - creatureStats.getActiveSpells().visitEffectSources(visitor); - - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - // tickable effects (i.e. effects having a lasting impact after expiry) - effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); - - // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities - if (it->second.getMagnitude() > 0) - { - CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) - { - creatureStats.getSpells().purgeEffect(it->first.mId); - creatureStats.getActiveSpells().purgeEffect(it->first.mId); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); - } - } - } - } - - // purge levitate effect if levitation is disabled - // check only modifier, because base value can be setted from SetFlying console command. - if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) - { - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); - - if (ptr == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - } - } - - // dynamic stats - for (int i = 0; i < 3; ++i) - { - DynamicStat stat = creatureStats.getDynamic(i); - float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); - float drain = 0.f; - if (!godmode) - drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); - stat.setCurrentModifier(fortify - drain, - // Magicka can be decreased below zero due to a fortify effect wearing off - // Fatigue can be decreased below zero meaning the actor will be knocked out - i == 1 || i == 2); - - creatureStats.setDynamic(i, stat); - } - - // attributes - for(int i = 0;i < ESM::Attribute::Length;++i) - { - AttributeValue stat = creatureStats.getAttribute(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); - } - stat.setModifier(static_cast(fortify - drain - absorb)); - - creatureStats.setAttribute(i, stat); - } if (creatureStats.needToRecalcDynamicStats()) calculateDynamicStats(ptr); - - if (ptr == getPlayer()) - { - GetCorprusSpells getCorprusSpellsVisitor; - creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); - creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); - std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; - std::vector corprusSpellsToRemove; - - for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) - { - if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) - { - // Corprus effect expired, remove entry and restore stats. - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); - corprusSpellsToRemove.push_back(it->first); - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - continue; - } - - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) - { - it->second.mNextWorsening += CorprusStats::sWorseningPeriod; - GetCurrentMagnitudes getMagnitudesVisitor (it->first); - creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); - creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); - for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) - { - if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - attr.damage(-effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - it->second.mWorsenings[effectMagnitude.first.mArg] = 0; - } - else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - int currentDamage = attr.getDamage(); - if (currentDamage >= 0) - it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); - - it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; - attr.damage(effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - } - } - - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - } - } - - for (std::string& oldCorprusSpell : corprusSpellsToRemove) - { - creatureStats.removeCorprusSpell(oldCorprusSpell); - } - - for (std::string& newCorprusSpell : corprusSpells) - { - CorprusStats corprus; - for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; - - creatureStats.addCorprusSpell(newCorprusSpell, corprus); - } - } - - // AI setting modifiers - int creature = !ptr.getClass().isNpc(); - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); - - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - - if (!wasDead && creatureStats.isDead()) - { - // The actor was killed by a magic effect. Figure out if the player was responsible for it. - const ActiveSpells& spells = creatureStats.getActiveSpells(); - MWWorld::Ptr player = getPlayer(); - std::set playerFollowers; - getActorsSidingWith(player, playerFollowers); - - for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - bool actorKilled = false; - - const ActiveSpells::ActiveSpellParams& spell = it->second; - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - for (std::vector::const_iterator effectIt = spell.mEffects.begin(); - effectIt != spell.mEffects.end(); ++effectIt) - { - int effectId = effectIt->mEffectId; - bool isDamageEffect = false; - - int damageEffects[] = { - ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, - ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth - }; - - for (unsigned int i=0; iactorKilled(ptr, player); - actorKilled = true; - break; - } - } - } - - if (actorKilled) - break; - } - } - - // TODO: dirty flag for magic effects to avoid some unnecessary work below? - - // any value of calm > 0 will stop the actor from fighting - if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) - || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) - creatureStats.getAiSequence().stopCombat(); - - // Update bound effects - // Note: in vanilla MW multiple bound items of the same type can be created by different spells. - // As these extra copies are kinda useless this may or may not be important. - static std::map boundItemsMap; - if (boundItemsMap.empty()) - { - boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; - boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; - boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; - boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; - boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) - boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; - boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; - boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; - boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; - boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; - boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; - } - - if(ptr.getClass().hasInventoryStore(ptr)) - { - for (const auto& [effect, itemGmst] : boundItemsMap) - { - bool found = creatureStats.mBoundItems.find(effect) != creatureStats.mBoundItems.end(); - float magnitude = effects.get(effect).getMagnitude(); - if (found != (magnitude > 0)) - { - if (magnitude > 0) - creatureStats.mBoundItems.insert(effect); - else - creatureStats.mBoundItems.erase(effect); - - std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( - itemGmst)->mValue.getString(); - - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - - if (effect == ESM::MagicEffect::BoundGloves) - { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundRightGauntletID")->mValue.getString(); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - } - } - } - } - - // Summoned creature update visitor assumes the actor belongs to a cell. - // This assumption isn't always valid for the player character. - if (!ptr.isInCell()) - return; - - bool hasSummonEffect = false; - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - if (isSummoningEffect(it->first.mId)) - { - hasSummonEffect = true; - break; - } - } - - if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) - { - UpdateSummonedCreatures updateSummonedCreatures(ptr); - creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); - creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); - updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); - } - } - - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); - const MagicEffects &effects = npcStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - // skills - for(int i = 0;i < ESM::Skill::Length;++i) - { - SkillValue& skill = npcStats.getSkill(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); - } - skill.setModifier(static_cast(fortify - drain - absorb)); - } } bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) @@ -1606,7 +1058,7 @@ namespace MWMechanics void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - removeActor(ptr); + removeActor(ptr, true); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) @@ -1654,11 +1106,13 @@ namespace MWMechanics ctrl->setVisibility(visibilityRatio); } - void Actors::removeActor (const MWWorld::Ptr& ptr) + void Actors::removeActor (const MWWorld::Ptr& ptr, bool keepActive) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { + if(!keepActive) + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter); } @@ -1723,6 +1177,7 @@ namespace MWMechanics { if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) { + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter++); } @@ -1807,6 +1262,7 @@ namespace MWMechanics // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either // follow player or have a AIWander package with non-empty wander area. bool shouldAvoidCollision = isMoving; + bool shouldGiveWay = false; bool shouldTurnToApproachingActor = !isMoving; MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). const auto& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); @@ -1817,7 +1273,7 @@ namespace MWMechanics else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { if (!static_cast(package.get())->isStationary()) - shouldAvoidCollision = true; + shouldGiveWay = true; } else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) { @@ -1827,7 +1283,7 @@ namespace MWMechanics break; } } - if (!shouldAvoidCollision) + if (!shouldAvoidCollision && !shouldGiveWay) continue; osg::Vec2f baseSpeed = origMovement * maxSpeed; @@ -1836,14 +1292,14 @@ namespace MWMechanics const osg::Vec3f halfExtents = world->getHalfExtents(ptr); float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; - float timeToCollision = maxTimeToCheck; + float timeToCheck = maxTimeToCheck; + if (!shouldGiveWay && !aiSequence.isEmpty()) + timeToCheck = std::min(timeToCheck, getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents)); + + float timeToCollision = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; - const float timeToDestination = aiSequence.isEmpty() - ? std::numeric_limits::max() - : getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents); - // Iterate through all other actors and predict collisions. for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) { @@ -1880,7 +1336,7 @@ namespace MWMechanics continue; // No solution; distance is always >= collisionDist. float t = (-vr - std::sqrt(Dh)) / v2; - if (t < 0 || t > timeToCollision || t > timeToDestination) + if (t < 0 || t > timeToCollision) continue; // Check visibility and awareness last as it's expensive. @@ -1900,7 +1356,7 @@ namespace MWMechanics movementCorrection.y() *= 0.5f; } - if (timeToCollision < maxTimeToCheck) + if (timeToCollision < timeToCheck) { // Try to evade the nearest collision. osg::Vec2f newMovement = origMovement + movementCorrection; @@ -1978,8 +1434,6 @@ namespace MWMechanics player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - const Misc::TimerStatus engageCombatTimerStatus = iter->second->updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles @@ -1987,7 +1441,7 @@ namespace MWMechanics { // They can be added during the death animation if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) - adjustMagicEffects(iter->first); + adjustMagicEffects(iter->first, duration); ctrl->updateContinuousVfx(); } else @@ -2082,8 +1536,6 @@ namespace MWMechanics if (inProcessingRange) updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - calculateNpcStatModifiers(iter->first, duration); - if (timerUpdateEquippedLight == 0) updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); } @@ -2156,7 +1608,14 @@ namespace MWMechanics continue; } else if (!isPlayer) + { iter->first.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); + if (!iter->second->getPositionAdjusted()) + { + iter->first.getClass().adjustPosition(iter->first, false); + iter->second->setPositionAdjusted(true); + } + } const bool isDead = iter->first.getClass().getCreatureStats(iter->first).isDead(); if (!isDead && (!godmode || !isPlayer) && iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) @@ -2250,10 +1709,7 @@ namespace MWMechanics // Apply soultrap if (iter->first.getTypeName() == typeid(ESM::Creature).name()) - { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); - } + soulTrap(iter->first); // Magic effects will be reset later, and the magic effect that could kill the actor // needs to be determined now @@ -2269,30 +1725,27 @@ namespace MWMechanics // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.modifyMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); + float vampirism = stats.getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude(); + stats.getActiveSpells().clear(iter->first); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); // Reset dynamic stats, attributes and skills calculateCreatureStatModifiers(iter->first, 0); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, 0); + stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { //player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); + // Play Death Music if it was the player dying + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } else { // NPC death animation is over, disable actor collision MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); } - - // Play Death Music if it was the player dying - if(iter->first == getPlayer()) - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } @@ -2312,7 +1765,7 @@ namespace MWMechanics // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2333,7 +1786,7 @@ namespace MWMechanics for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); - spells.purge(casterActorId); + spells.purge(iter->first, casterActorId); } } @@ -2351,7 +1804,7 @@ namespace MWMechanics { if (iter->first.getClass().getCreatureStats(iter->first).isDead()) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + adjustMagicEffects (iter->first, duration); continue; } @@ -2362,15 +1815,11 @@ namespace MWMechanics (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) continue; - adjustMagicEffects (iter->first); + adjustMagicEffects (iter->first, duration); if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) calculateDynamicStats (iter->first); calculateCreatureStatModifiers (iter->first, duration); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, duration); - - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2759,10 +2208,8 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { - adjustMagicEffects(ptr); + adjustMagicEffects(ptr, 0.f); calculateCreatureStatModifiers(ptr, 0.f); - if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 171b45e270..9950a591ab 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -41,15 +41,11 @@ namespace MWMechanics { std::map mDeathCount; - void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - - void adjustMagicEffects (const MWWorld::Ptr& creature); + void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); @@ -94,7 +90,7 @@ namespace MWMechanics /// /// \note Dead actors are ignored. - void removeActor (const MWWorld::Ptr& ptr); + void removeActor (const MWWorld::Ptr& ptr, bool keepActive); ///< Deregister an actor for stats management /// /// \note Ignored, if \a ptr is not a registered actor. @@ -208,7 +204,6 @@ namespace MWMechanics private: void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); - void applyCureEffects (const MWWorld::Ptr& actor); PtrActorMap mActors; float mTimerDisposeSummonsCorpses; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 4b490d7bc8..eb139c918d 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -348,7 +348,7 @@ namespace MWMechanics bool runFallback = true; - if (pathgrid && !actor.getClass().isPureWaterCreature(actor)) + if (pathgrid != nullptr && !pathgrid->mPoints.empty() && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; Misc::CoordinateConverter coords(storage.mCell->getCell()); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index c26454aab5..e6176c869c 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -208,14 +208,14 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionSpell(it->first->mId)); - antiFleeRating = vanillaRateSpell(it->first, actor, enemy); + bestAction.reset(new ActionSpell(spell->mId)); + antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } @@ -266,9 +266,9 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 46e4729a8a..b1f4355a39 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -34,6 +34,11 @@ namespace const float actorTolerance = 2 * speed * duration + 1.2 * std::max(halfExtents.x(), halfExtents.y()); return std::max(MWMechanics::MIN_TOLERANCE, actorTolerance); } + + bool canOpenDoors(const MWWorld::Ptr& ptr) + { + return ptr.getClass().isBipedal(ptr) || ptr.getClass().hasInventoryStore(ptr); + } } MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : @@ -118,7 +123,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { - if (actor.getClass().isBipedal(actor)) + if (canOpenDoors(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; @@ -232,7 +237,7 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (!door.isEmpty() && actor.getClass().isBipedal(actor)) + if (!door.isEmpty() && canOpenDoors(actor)) { openDoors(actor); } @@ -443,9 +448,13 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) + { result |= DetourNavigator::Flag_walk; + if (getTypeId() == AiPackageTypeId::Travel) + result |= DetourNavigator::Flag_usePathgrid; + } - if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) + if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; @@ -457,20 +466,31 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - if (flags & DetourNavigator::Flag_swim) - costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); + const float swimSpeed = (flags & DetourNavigator::Flag_swim) == 0 + ? 0.0f + : actorClass.getSwimSpeed(actor); - if (flags & DetourNavigator::Flag_walk) + const float walkSpeed = [&] { - float walkCost; + if ((flags & DetourNavigator::Flag_walk) == 0) + return 0.0f; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); - else - walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); - costs.mDoor = costs.mDoor * walkCost; - costs.mPathgrid = costs.mPathgrid * walkCost; - costs.mGround = costs.mGround * walkCost; - } + return actorClass.getWalkSpeed(actor); + return actorClass.getRunSpeed(actor); + } (); + + const float maxSpeed = std::max(swimSpeed, walkSpeed); + + if (maxSpeed == 0) + return costs; + + const float swimFactor = swimSpeed / maxSpeed; + const float walkFactor = walkSpeed / maxSpeed; + + costs.mWater = divOrMax(costs.mWater, swimFactor); + costs.mDoor = divOrMax(costs.mDoor, walkFactor); + costs.mPathgrid = divOrMax(costs.mPathgrid, walkFactor); + costs.mGround = divOrMax(costs.mGround, walkFactor); return costs; } diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 91fe96a2aa..2594adcb34 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,5 +1,7 @@ #include "aitravel.hpp" +#include + #include #include "../mwbase/environment.hpp" @@ -23,6 +25,11 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) return (pos1 - pos2).length2() <= 7168*7168; } + float getActorRadius(const MWWorld::ConstPtr& actor) + { + const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + return std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + } } namespace MWMechanics @@ -70,16 +77,24 @@ namespace MWMechanics // Unfortunately, with vanilla assets destination is sometimes blocked by other actor. // If we got close to target, check for actors nearby. If they are, finish AI package. - int destinationTolerance = 64; - if (distance(actorPos, targetPos) <= destinationTolerance) + if (mDestinationCheck.update(duration) == Misc::TimerStatus::Elapsed) { - std::vector targetActors; - std::pair result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors); - - if (!result.first.isEmpty()) + std::vector occupyingActors; + if (isAreaOccupiedByOtherActor(actor, targetPos, &occupyingActors)) { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - return true; + const float actorRadius = getActorRadius(actor); + const float distanceToTarget = distance(actorPos, targetPos); + for (const MWWorld::Ptr& other : occupyingActors) + { + const float otherRadius = getActorRadius(other); + const auto [minRadius, maxRadius] = std::minmax(actorRadius, otherRadius); + constexpr float toleranceFactor = 1.25; + if (minRadius * toleranceFactor + maxRadius > distanceToTarget) + { + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + return true; + } + } } } diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 2ea2a8f717..ee4ff9ad4d 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -52,6 +52,8 @@ namespace MWMechanics const float mZ; const bool mHidden; + + AiReactionTimer mDestinationCheck; }; struct AiInternalTravel final : public AiTravel diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 19365bcb08..9c84555404 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -85,14 +85,6 @@ namespace MWMechanics return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor); } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor); - } - void stopMovement(const MWWorld::Ptr& actor) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; @@ -758,6 +750,9 @@ namespace MWMechanics const ESM::Pathgrid *pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*currentCell->getCell()); + if (pathgrid == nullptr || pathgrid->mPoints.empty()) + return; + int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(currentCell).getNeighbouringPoints(index, points); diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 8d99664f27..8ca6e8b766 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -545,20 +545,12 @@ namespace MWMechanics mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); - state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; for (int i=0; i<4; ++i) mAiSettings[i].writeState (state.mAiSettings[i]); - - for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; - - state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); - } } void CreatureStats::readState (const ESM::CreatureStats& state) @@ -618,7 +610,7 @@ namespace MWMechanics // We can't use mActiveSpells::getMagicEffects here because it doesn't include expired effects auto spell = std::find_if(mActiveSpells.begin(), mActiveSpells.end(), [&] (const auto& spell) { - const auto& effects = spell.second.mEffects; + const auto& effects = spell.getEffects(); return std::find_if(effects.begin(), effects.end(), [&] (const auto& effect) { return effect.mEffectId == effectId; @@ -629,21 +621,12 @@ namespace MWMechanics } } - mSummonedCreatures = state.mSummonedCreatureMap; + mSummonedCreatures = state.mSummonedCreatures; mSummonGraveyard = state.mSummonGraveyard; if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); - - mCorprusSpells.clear(); - for (auto it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; - - mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - } } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -710,7 +693,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -719,23 +702,4 @@ namespace MWMechanics { return mSummonGraveyard; } - - std::map &CreatureStats::getCorprusSpells() - { - return mCorprusSpells; - } - - void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) - { - mCorprusSpells[sourceId] = stats; - } - - void CreatureStats::removeCorprusSpell(const std::string& sourceId) - { - auto corprusIt = mCorprusSpells.find(sourceId); - if (corprusIt != mCorprusSpells.end()) - { - mCorprusSpells.erase(corprusIt); - } - } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 06d526cc5a..d234d14486 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H +#include #include #include #include @@ -87,14 +88,12 @@ namespace MWMechanics float mSideMovementAngle; private: - std::map mSummonedCreatures; // + std::multimap mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; - std::map mCorprusSpells; - protected: int mLevel; @@ -236,7 +235,7 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag @@ -297,12 +296,6 @@ namespace MWMechanics static void cleanup(); - std::map & getCorprusSpells(); - - void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); - - void removeCorprusSpell(const std::string& sourceId); - float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 5d4bfb682f..426a66ecf2 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -30,12 +30,11 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->mValue.getFloat(); - MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; @@ -56,7 +55,7 @@ namespace MWMechanics if (Misc::Rng::rollDice(10000) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); std::string msg = "sMagicContractDisease"; diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp deleted file mode 100644 index b0defac7d2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "linkedeffects.hpp" - -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } - - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) - { - if (caster.isEmpty() || caster == target) - return; - - if (!target.getClass().isActor() || !caster.getClass().isActor()) - return; - - // Make sure callers don't do something weird - if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) - throw std::runtime_error("invalid absorb stat effect"); - - if (appliedEffect.mMagnitude == 0) - return; - - std::vector absorbEffects; - ActiveSpells::ActiveEffect absorbEffect = appliedEffect; - absorbEffect.mMagnitude *= -1; - absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; - absorbEffects.emplace_back(absorbEffect); - - // Morrowind negates reflected Absorb spells so the original caster won't be harmed. - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - { - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); - return; - } - - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); - } -} diff --git a/apps/openmw/mwmechanics/linkedeffects.hpp b/apps/openmw/mwmechanics/linkedeffects.hpp deleted file mode 100644 index a6dea2a3a2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MWMECHANICS_LINKEDEFFECTS_H -#define MWMECHANICS_LINKEDEFFECTS_H - -#include - -namespace ESM -{ - struct ActiveEffect; - struct EffectList; - struct ENAMstruct; - struct MagicEffect; - struct Spell; -} - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - - // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); - - // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); -} - -#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 1a1a44f638..e75361628b 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -193,22 +193,22 @@ namespace MWMechanics void MagicEffects::writeState(ESM::MagicEffects &state) const { - // Don't need to save Modifiers, they are recalculated every frame anyway. - for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + for (const auto& [key, params] : mCollection) { - if (iter->second.getBase() != 0) + if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions - state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + state.mEffects[key.mId] = {params.getBase(), params.getModifier()}; } } } void MagicEffects::readState(const ESM::MagicEffects &state) { - for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + for (const auto& [key, params] : state.mEffects) { - mCollection[EffectKey(it->first)].setBase(it->second); + mCollection[EffectKey(key)].setBase(params.first); + mCollection[EffectKey(key)].setModifier(params.second); } } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 12735a87fc..50f4dab050 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -69,16 +69,6 @@ namespace MWMechanics return param -= right; } - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual ~EffectSourceVisitor() { } - - virtual void visit (EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) = 0; - }; - /// \brief Effects currently affecting a NPC or creature class MagicEffects { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 24601d79c8..5e18e737df 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -84,7 +84,7 @@ namespace MWMechanics // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); - creatureStats.modifyMagicEffects(MagicEffects()); + creatureStats.getActiveSpells().clear(ptr); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); @@ -213,6 +213,7 @@ namespace MWMechanics int attributes[ESM::Attribute::Length]; for (int i=0; i selectedSpells = autoCalcPlayerSpells(skills, attributes, race); @@ -221,6 +222,7 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); + mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { @@ -257,11 +259,11 @@ namespace MWMechanics mActors.castSpell(ptr, spellId, manualSpell); } - void MechanicsManager::remove(const MWWorld::Ptr& ptr) + void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) { if(ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); - mActors.removeActor(ptr); + mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } @@ -282,24 +284,6 @@ namespace MWMechanics mObjects.dropObjects(cellStore); } - void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) - { - auto& stats = actor.getClass().getCreatureStats (actor); - auto& corprusSpells = stats.getCorprusSpells(); - - auto corprusIt = corprusSpells.find(sourceId); - - if (corprusIt != corprusSpells.end()) - { - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - MWMechanics::AttributeValue attr = stats.getAttribute(i); - attr.restore(corprusIt->second.mWorsenings[i]); - actor.getClass().getCreatureStats(actor).setAttribute(i, attr); - } - } - } - void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values @@ -333,7 +317,7 @@ namespace MWMechanics // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. - mActors.removeActor(ptr); + mActors.removeActor(ptr, true); mActors.addActor(ptr, true); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 0aaeb23435..06da2fde51 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -45,7 +45,7 @@ namespace MWMechanics void add (const MWWorld::Ptr& ptr) override; ///< Register an object for management - void remove (const MWWorld::Ptr& ptr) override; + void remove (const MWWorld::Ptr& ptr, bool keepActive) override; ///< Deregister an object for management void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; @@ -230,8 +230,6 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; - void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; - private: bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 88325ee7c7..344cc82058 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -4,6 +4,8 @@ #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "movement.hpp" @@ -72,6 +74,15 @@ namespace MWMechanics return MWWorld::Ptr(); // none found } + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, + std::vector* occupyingActors) + { + const auto world = MWBase::Environment::get().getWorld(); + const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); + const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor, occupyingActors); + } + ObstacleCheck::ObstacleCheck() : mWalkState(WalkState::Initial) , mStateDuration(0) diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index b574bab67f..24bd5ed1c1 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -3,9 +3,12 @@ #include +#include + namespace MWWorld { class Ptr; + class ConstPtr; } namespace MWMechanics @@ -21,6 +24,9 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, + std::vector* occupyingActors = nullptr); + class ObstacleCheck { public: diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 7e63190edb..4b06993a49 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -397,7 +397,7 @@ namespace MWMechanics mPath.clear(); } - if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty()) + if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty() && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, std::back_inserter(mPath)); diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp index cb80921b33..82531132ca 100644 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ b/apps/openmw/mwmechanics/spellabsorption.cpp @@ -16,33 +16,30 @@ namespace MWMechanics { - - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor + float getProbability(const MWMechanics::ActiveSpells& activeSpells) { - public: - float mProbability{0.f}; - - GetAbsorptionProbability() = default; - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float /*remainingTime*/, float /*totalTime*/) override + float probability = 0.f; + for(const auto& params : activeSpells) { - if (key.mId == ESM::MagicEffect::SpellAbsorption) + for(const auto& effect : params.getEffects()) { - if (mProbability == 0.f) - mProbability = magnitude / 100; - else + if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - mProbability; - failProbability *= 1.f - magnitude / 100; - mProbability = 1.f - failProbability; + if(probability == 0.f) + probability = effect.mMagnitude / 100; + else + { + // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. + // Real absorption probability will be the (1 - total fail chance) in this case. + float failProbability = 1.f - probability; + failProbability *= 1.f - effect.mMagnitude / 100; + probability = 1.f - failProbability; + } } } } - }; + return static_cast(probability * 100); + } bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) { @@ -53,13 +50,7 @@ namespace MWMechanics if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) return false; - GetAbsorptionProbability check; - stats.getActiveSpells().visitEffectSources(check); - stats.getSpells().visitEffectSources(check); - if (target.getClass().hasInventoryStore(target)) - target.getClass().getInventoryStore(target).visitEffectSources(check); - - int chance = check.mProbability * 100; + int chance = getProbability(stats.getActiveSpells()); if (Misc::Rng::roll0to99() >= chance) return false; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 502e56edf7..53fbe69ab3 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,14 +22,38 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "linkedeffects.hpp" #include "spellabsorption.hpp" -#include "spellresistance.hpp" +#include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" -#include "tickableeffects.hpp" #include "weapontype.hpp" +namespace +{ + bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, + const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) + { + if (caster.isEmpty() || caster == target || !target.getClass().isActor()) + return false; + + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; + if (!isHarmful || isUnreflectable) + return false; + + float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); + if (Misc::Rng::roll0to99() >= reflect) + return false; + + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation && !reflectStatic->mModel.empty()) + animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); + reflectedEffects.mList.emplace_back(effect); + return true; + } +} + namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) @@ -54,7 +78,7 @@ namespace MWMechanics (mTarget.getRefData().getPosition().asVec3() + offset) - (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mSlot); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, @@ -100,20 +124,13 @@ namespace MWMechanics } ESM::EffectList reflectedEffects; - std::vector appliedLastingEffects; - - // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. - // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. - // Otherwise, they'd only apply after the whole spell was added. - MagicEffects targetEffects; - if (targetIsActor) - targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); + ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); - ActiveSpells targetSpells; + const ActiveSpells* targetSpells = nullptr; if (targetIsActor) - targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); + targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); bool canCastAnEffect = false; // For bound equipment.If this remains false // throughout the iteration of this spell's @@ -134,7 +151,7 @@ namespace MWMechanics effectIt->mEffectID); // Re-casting a bound equipment effect has no effect if the spell is still active - if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) + if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells && targetSpells->isSpellActive(mId)) { if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); @@ -163,307 +180,70 @@ namespace MWMechanics if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) continue; - // Try resisting. - float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); - if (magnitudeMult == 0) + ActiveSpells::ActiveEffect effect; + effect.mEffectId = effectIt->mEffectID; + effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = effectIt->mMagnMin; + effect.mMaxMagnitude = effectIt->mMagnMax; + effect.mTimeLeft = 0.f; + effect.mEffectIndex = currentEffectIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + + // Avoid applying harmful effects to the player in god mode + if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + effect.mMinMagnitude = 0; + effect.mMaxMagnitude = 0; } - else + + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; + + bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + + effect.mTimeLeft = effect.mDuration; + + // add to list of active effects, to apply in next frame + params.getEffects().emplace_back(effect); + + bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { - float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); - magnitude *= magnitudeMult; + // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. + MWBase::Environment::get().getWindowManager()->setEnemy(target); + } - if (!target.getClass().isActor()) - { - // non-actor objects have no list of active magic effects, so have to apply instantly - if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) - continue; - } - else // target.getClass().isActor() == true - { - ActiveSpells::ActiveEffect effect; - effect.mEffectId = effectIt->mEffectID; - effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; - effect.mMagnitude = magnitude; - effect.mTimeLeft = 0.f; - effect.mEffectIndex = currentEffectIndex; - - // Avoid applying absorb effects if the caster is the target - // We still need the spell to be added - if (caster == target - && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute - && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - { - effect.mMagnitude = 0; - } - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMagnitude = 0; - } - - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) - { - // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. - MWBase::Environment::get().getWindowManager()->setEnemy(target); - } - - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; - - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); - - if (effect.mDuration == 0) - { - // We still should add effect to list to allow GetSpellEffects to detect this spell - appliedLastingEffects.push_back(effect); - - // duration 0 means apply full magnitude instantly - bool wasDead = target.getClass().getCreatureStats(target).isDead(); - effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); - bool isDead = target.getClass().getCreatureStats(target).isDead(); - - if (!wasDead && isDead) - MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); - } - else - { - effect.mTimeLeft = effect.mDuration; - - targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); - - // add to list of active effects, to apply in next frame - appliedLastingEffects.push_back(effect); - - // Unequip all items, if a spell with the ExtraSpell effect was casted - if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) - { - MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); - store.unequipAll(target); - } - - // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target - if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) - || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) - && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) - { - MWMechanics::AiFollow package(caster, true); - target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); - } - - // For absorb effects, also apply the effect to the caster - but with a negative - // magnitude, since we're transferring stats from the target to the caster - if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); - } - } - - // Re-casting a summon effect will remove the creature from previous castings of that effect. - if (isSummoningEffect(effectIt->mEffectID) && targetIsActor) - { - CreatureStats& targetStats = target.getClass().getCreatureStats(target); - ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); - auto findCreature = targetStats.getSummonedCreatureMap().find(key); - if (findCreature != targetStats.getSummonedCreatureMap().end()) - { - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); - targetStats.getSummonedCreatureMap().erase(findCreature); - } - } - - if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - - // Add VFX - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Note: in case of non actor, a free effect should be fine as well - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } + if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + { + playEffects(target, *magicEffect); } } if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); + MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile, mSlot); if (!target.isEmpty()) { if (!reflectedEffects.mList.empty()) inflict(caster, target, reflectedEffects, range, true, exploded); - if (!appliedLastingEffects.empty()) + if (!params.getEffects().empty()) { - int casterActorId = -1; - if (!caster.isEmpty() && caster.getClass().isActor()) - casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, - mSourceName, casterActorId); - } - } - } - - bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) - { - short effectId = effect.mId; - if (target.getClass().canLock(target)) - { - if (effectId == ESM::MagicEffect::Lock) - { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); - target.getCellRef().lock(static_cast(magnitude)); - } - return true; - } - else if (effectId == ESM::MagicEffect::Open) - { - if (!caster.isEmpty()) - { - MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); - // Use the player instead of the caster for vanilla crime compatibility - } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() <= magnitude) - { - if (target.getCellRef().getLockLevel() > 0) - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); - } - target.getCellRef().unlock(); - } + if(targetIsActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); else { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + for(auto& effect : params.getEffects()) + applyMagicEffect(target, caster, params, effect, 0.f); } - - return true; } } - else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) - { - target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); - return true; - } - else if(target.getClass().isActor() && effectId >= ESM::MagicEffect::CalmHumanoid && effectId <= ESM::MagicEffect::RallyCreature) - { - // Treat X Humanoid spells on creatures and X Creature spells on NPCs as instant effects and remove their VFX - bool affectsCreatures = (effectId - ESM::MagicEffect::CalmHumanoid) & 1; - if(affectsCreatures == target.getClass().isNpc()) - { - MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId); - return true; - } - } - else if(target.getClass().isActor() && effectId == ESM::MagicEffect::TurnUndead) - { - // Diverge from vanilla by giving scripts a chance to detect Turn Undead on non-undead, but still remove the effect and VFX - if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) - { - MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId); - return true; - } - } - else if (target.getClass().isActor() && target == getPlayer()) - { - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); - - if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; - MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); - anim->removeEffect(effectId); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_end"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1); - return true; - } - else if (effectId == ESM::MagicEffect::Mark) - { - if (teleportingEnabled) - { - MWBase::Environment::get().getWorld()->getPlayer().markPosition( - target.getCell(), target.getRefData().getPosition()); - } - else if (caster == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - } - return true; - } - else if (effectId == ESM::MagicEffect::Recall) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - - MWWorld::CellStore* markedCell = nullptr; - ESM::Position markedPosition; - - MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell) - { - MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, - markedPosition, false); - action.execute(target); - anim->removeEffect(effectId); - } - return true; - } - } - return false; } - bool CastSpell::cast(const std::string &id) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -479,7 +259,7 @@ namespace MWMechanics throw std::runtime_error("ID type cannot be casted"); } - bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) + bool CastSpell::cast(const MWWorld::Ptr &item, int slot, bool launchProjectile) { std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) @@ -490,7 +270,7 @@ namespace MWMechanics const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); - mStack = false; + mSlot = slot; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; @@ -570,7 +350,7 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); @@ -581,7 +361,6 @@ namespace MWMechanics { mSourceName = spell->mName; mId = spell->mId; - mStack = false; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -656,7 +435,7 @@ namespace MWMechanics bool CastSpell::cast (const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; mSourceName = ingredient->mName; ESM::ENAMstruct effect; @@ -799,4 +578,36 @@ namespace MWMechanics sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } } + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping) + { + if (playNonLooping) + { + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!magicEffect.mHitSound.empty()) + sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(target, schools[magicEffect.mData.mSchool]+" hit", 1.0f, 1.0f); + } + + // Add VFX + const ESM::Static* castStatic; + if (!magicEffect.mHit.empty()) + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect.mHit); + else + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); + + bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim && !castStatic->mModel.empty()) + { + // Don't play particle VFX unless the effect is new or it should be looping. + if (playNonLooping || loop) + anim->addEffect("meshes\\" + castStatic->mModel, magicEffect.mIndex, loop, "", magicEffect.mParticle); + } + } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index cc525d2db6..a21ea33e0b 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -1,6 +1,7 @@ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H +#include #include #include "../mwworld/ptr.hpp" @@ -11,6 +12,7 @@ namespace ESM struct Ingredient; struct Potion; struct EffectList; + struct MagicEffect; } namespace MWMechanics @@ -26,13 +28,14 @@ namespace MWMechanics void playSpellCastingEffects(const std::vector& effects); public: - bool mStack{false}; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) + int mSlot{0}; + ESM::ActiveSpells::EffectType mType{ESM::ActiveSpells::Type_Temporary}; public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); @@ -41,7 +44,7 @@ namespace MWMechanics /// @note mCaster must be an actor /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. - bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); + bool cast (const MWWorld::Ptr& item, int slot, bool launchProjectile=true); /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); @@ -60,11 +63,9 @@ namespace MWMechanics /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); - - /// @note \a caster can be any type of object, or even an empty object. - /// @return was the target suitable for the effect? - bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); }; + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); } #endif diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp new file mode 100644 index 0000000000..d9e72ee43b --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -0,0 +1,1041 @@ +#include "spelleffects.hpp" + +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/summoning.hpp" + +#include "../mwrender/animation.hpp" + +#include "../mwworld/actionequip.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace +{ + float roll(const ESM::ActiveEffect& effect) + { + if(effect.mMinMagnitude == effect.mMaxMagnitude) + return effect.mMinMagnitude; + return effect.mMinMagnitude + Misc::Rng::rollDice(effect.mMaxMagnitude - effect.mMinMagnitude + 1); + } + + void modifyAiSetting(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, ESM::MagicEffect::Effects creatureEffect, MWMechanics::CreatureStats::AiSetting setting, float magnitude, bool& invalid) + { + if(target == MWMechanics::getPlayer() || (effect.mEffectId == creatureEffect) == target.getClass().isNpc()) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getAiSetting(setting); + stat.setModifier(static_cast(stat.getModifier() - magnitude)); + creatureStats.setAiSetting(setting, stat); + } + } + + void adjustDynamicStat(const MWWorld::Ptr& target, int index, float magnitude, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero, allowIncreaseAboveModified); + creatureStats.setDynamic(index, stat); + } + + void modDynamicStat(const MWWorld::Ptr& target, int index, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + float current = stat.getCurrent(); + stat.setModified(stat.getModified() + magnitude, 0); + stat.setCurrentModified(stat.getCurrentModified() + magnitude); + stat.setCurrent(current + magnitude); + creatureStats.setDynamic(index, stat); + } + + void damageAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.damage(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void restoreAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.restore(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void fortifyAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.setModifier(attr.getModifier() + magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void damageSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.damage(magnitude); + } + + void restoreSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.restore(magnitude); + } + + void fortifySkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setModifier(skill.getModifier() + magnitude); + } + + bool disintegrateSlot(const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != MWMechanics::getPlayer()) + inv.autoEquip(ptr); + else + inv.unequipItem(*item, ptr); + } + + return true; + } + + return false; + } + + int getBoundItemSlot(const MWWorld::Ptr& boundPtr) + { + const auto [slots, _] = boundPtr.getClass().getEquipmentSlots(boundPtr); + if(!slots.empty()) + return slots[0]; + return -1; + } + + void addBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + + int slot = getBoundItemSlot(boundPtr); + auto prevItem = slot >= 0 ? store.getSlot(slot) : store.end(); + + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem; + auto it = slot >= 0 ? store.getSlot(slot) : store.end(); + // Equip can fail because beast races cannot equip boots/helmets + if(it != store.end()) + newItem = *it; + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState_Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void removeBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + auto item = std::find_if(store.begin(), store.end(), [&] (const auto& it) + { + return Misc::StringUtils::ciEqual(it.getCellRef().getRefId(), itemId); + }); + if(item == store.end()) + return; + int slot = getBoundItemSlot(*item); + + auto currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); + + if (actor != MWMechanics::getPlayer()) + { + store.remove(itemId, 1, actor); + + // Equip a replacement + if (!wasEquipped) + return; + + std::string type = currentItem->getTypeName(); + if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) + return; + + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + + if (!actor.getClass().hasInventoryStore(actor)) + return; + + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return; + + actor.getClass().getInventoryStore(actor).autoEquip(actor); + + return; + } + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (!prevItemId.empty() && wasEquipped) + { + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr prevItem = store.findReplacement(prevItemId); + if (!prevItem.isEmpty()) + { + MWWorld::ActionEquip action(prevItem); + action.execute(actor); + } + } + + store.remove(itemId, 1, actor); + } + + bool isCorprusEffect(const MWMechanics::ActiveSpells::ActiveEffect& effect, bool harmfulOnly = false) + { + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId != ESM::MagicEffect::Corprus) + { + const auto* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce && (!harmfulOnly || magicEffect->mData.mFlags & ESM::MagicEffect::Flags::Harmful)) + return true; + } + return false; + } + + static const std::map sBoundItemsMap{ + {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, + {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, + {ESM::MagicEffect::BoundCuirass, "sMagicBoundCuirassID"}, + {ESM::MagicEffect::BoundDagger, "sMagicBoundDaggerID"}, + {ESM::MagicEffect::BoundGloves, "sMagicBoundLeftGauntletID"}, + {ESM::MagicEffect::BoundHelm, "sMagicBoundHelmID"}, + {ESM::MagicEffect::BoundLongbow, "sMagicBoundLongbowID"}, + {ESM::MagicEffect::BoundLongsword, "sMagicBoundLongswordID"}, + {ESM::MagicEffect::BoundMace, "sMagicBoundMaceID"}, + {ESM::MagicEffect::BoundShield, "sMagicBoundShieldID"}, + {ESM::MagicEffect::BoundSpear, "sMagicBoundSpearID"} + }; +} + +namespace MWMechanics +{ + +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool godmode = target == getPlayer() && world->getGodModeState(); + switch(effect.mEffectId) + { + case ESM::MagicEffect::CureCommonDisease: + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + break; + case ESM::MagicEffect::CureCorprusDisease: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Corprus); + break; + case ESM::MagicEffect::CurePoison: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::Dispel: + // Dispel removes entire spells at once + target.getClass().getCreatureStats(target).getActiveSpells().purge([magnitude=effect.mMagnitude] (const ActiveSpells::ActiveSpellParams& params) + { + if(params.getType() == ESM::ActiveSpells::Type_Temporary) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(params.getId()); + if(spell && spell->mData.mType == ESM::Spell::ST_Spell) + return Misc::Rng::roll0to99() < magnitude; + } + return false; + }, target); + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + auto marker = (effect.mEffectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; + world->teleportToClosestMarker(target, marker); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_end"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1); + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Mark: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + world->getPlayer().markPosition(target.getCell(), target.getRefData().getPosition()); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Recall: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + MWWorld::CellStore* markedCell = nullptr; + ESM::Position markedPosition; + + world->getPlayer().getMarkedPosition(markedCell, markedPosition); + if (markedCell) + { + MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, markedPosition, false); + action.execute(target); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(caster.isEmpty() || !caster.getClass().isActor() || target == getPlayer() || (effect.mEffectId == ESM::MagicEffect::CommandCreature) == target.getClass().isNpc()) + invalid = true; + else if(effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) + { + MWMechanics::AiFollow package(caster, true); + target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(target.getClass().hasInventoryStore(target)) + { + auto& store = target.getClass().getInventoryStore(target); + store.unequipAll(target); + } + else + invalid = true; + break; + case ESM::MagicEffect::TurnUndead: + if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() + effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + if(!invalid && effect.mMagnitude > 0) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + creatureStats.getAiSequence().stopCombat(); + } + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + if(!target.isInCell()) + invalid = true; + else + effect.mArg = summonCreature(effect.mEffectId, target); + break; + case ESM::MagicEffect::BoundGloves: + if(!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + addBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + // left gauntlet added below + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + if(!target.getClass().hasInventoryStore(target)) + invalid = true; + else + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + addBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::DamageHealth: + case ESM::MagicEffect::Poison: + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + if(!godmode) + { + int index = 0; + if(effect.mEffectId == ESM::MagicEffect::DamageMagicka) + index = 1; + else if(effect.mEffectId == ESM::MagicEffect::DamageFatigue) + index = 2; + // Damage "Dynamic" abilities reduce the base value + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, index, -effect.mMagnitude); + else + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + adjustDynamicStat(target, index, -effect.mMagnitude, index == 2 && uncappedDamageFatigue); + if(index == 0) + receivedMagicDamage = true; + } + } + break; + case ESM::MagicEffect::DamageAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DamageSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + // Damage Skill abilities reduce base skill :todd: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + SkillValue& skill = npcStats.getSkill(effect.mArg); + // Damage Skill abilities reduce base skill :todd: + skill.setBase(std::max(skill.getBase() - effect.mMagnitude, 0.f)); + } + else + damageSkill(target, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::RestoreAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreSkill: + if(!target.getClass().isNpc()) + invalid = true; + else + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::RestoreHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::SunDamage: + { + // isInCell shouldn't be needed, but updateActor called during game start + if (!target.isInCell() || !target.getCell()->isExterior() || godmode) + break; + float time = world->getTimeStamp().getHour(); + float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float damageScale = 1.f - timeDiff / 7.f; + // When cloudy, the sun damage effect is halved + static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); + + int weather = world->getCurrentWeather(); + if (weather > 1) + damageScale *= fMagicSunBlockedMult; + float damage = effect.mMagnitude * damageScale; + adjustDynamicStat(target, 0, -damage); + if (damage > 0.f) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + if(!godmode) + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; + adjustDynamicStat(target, index, -effect.mMagnitude, uncappedDamageFatigue && index == 2); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude, false, true); + break; + case ESM::MagicEffect::DrainAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() + effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + damageSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + // Abilities affect base stats, but not for drain + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() + effect.mMagnitude); + } + else + fortifySkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbHealth: + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + if(!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::AbsorbHealth; + adjustDynamicStat(target, index, -effect.mMagnitude); + if(!caster.isEmpty()) + adjustDynamicStat(caster, index, effect.mMagnitude); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::AbsorbAttribute: + if(!godmode) + { + damageAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + damageSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::DisintegrateArmor: + { + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (godmode) + break; + static const std::array priorities + { + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots + }; + for (const int priority : priorities) + { + if (disintegrateSlot(target, priority, effect.mMagnitude)) + break; + } + break; + } + case ESM::MagicEffect::DisintegrateWeapon: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (!godmode) + disintegrateSlot(target, MWWorld::InventoryStore::Slot_CarriedRight, effect.mMagnitude); + break; + } +} + +bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool invalid = false; + bool receivedMagicDamage = false; + if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) + { + spellParams.worsen(); + for(auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect)) + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); + } + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + return false; + } + else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) + { + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); + return true; + } + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + { + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return false; + } + else if(!dt) + return false; + } + if(effect.mEffectId == ESM::MagicEffect::Lock) + { + if(target.getClass().canLock(target)) + { + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude + { + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); + target.getCellRef().lock(magnitude); + } + } + else + invalid = true; + } + else if(effect.mEffectId == ESM::MagicEffect::Open) + { + if(target.getClass().canLock(target)) + { + // Use the player instead of the caster for vanilla crime compatibility + MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); + + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() <= magnitude) + { + if(target.getCellRef().getLockLevel() > 0) + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); + } + target.getCellRef().unlock(); + } + else + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + } + } + else + invalid = true; + } + else if(!target.getClass().isActor()) + { + invalid = true; + } + else + { + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + { + const ESM::Spell* spell = nullptr; + if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + spell = world->getStore().get().search(spellParams.getId()); + float magnitudeMult = getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return true; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + float oldMagnitude = 0.f; + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + oldMagnitude = effect.mMagnitude; + float magnitude = roll(effect); + //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here + effect.mMagnitude = magnitude; + if(!(magicEffect->mData.mFlags & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) + { + if(effect.mDuration != 0) + { + float mult = dt; + if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + mult = std::min(effect.mTimeLeft, dt); + effect.mMagnitude *= mult; + } + if(effect.mMagnitude == 0) + { + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + effect.mTimeLeft -= dt; + return false; + } + } + if(effect.mEffectId == ESM::MagicEffect::Corprus) + spellParams.worsen(); + else + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + effect.mMagnitude = magnitude; + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); + } + effect.mTimeLeft -= dt; + if(invalid) + { + effect.mTimeLeft = 0; + effect.mFlags |= ESM::ActiveEffect::Flag_Remove; + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } + else + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + if (receivedMagicDamage && target == getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + return false; +} + +void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + bool invalid; + switch(effect.mEffectId) + { + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); + auto it = std::find_if(seq.begin(), seq.end(), [&](const auto& package) + { + return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(package.get())->isCommanded(); + }); + if(it != seq.end()) + seq.erase(it); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + target.getClass().getInventoryStore(target).autoEquip(target); + break; + case ESM::MagicEffect::TurnUndead: + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() - effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + { + if(effect.mArg != -1) + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, effect.mArg); + auto& summons = target.getClass().getCreatureStats(target).getSummonedCreatureMap(); + auto [begin, end] = summons.equal_range(effect.mEffectId); + for(auto it = begin; it != end; ++it) + { + if(it->second == effect.mArg) + { + summons.erase(it); + break; + } + } + } + break; + case ESM::MagicEffect::BoundGloves: + removeBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + removeBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::DrainHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude, true); + break; + case ESM::MagicEffect::DrainAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() - effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() - effect.mMagnitude); + } + else + fortifySkill(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbAttribute: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::Corprus: + { + int worsenings = spellParams.getWorsenings(); + spellParams.resetWorsenings(); + if(worsenings > 0) + { + for(const auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect, true)) + { + for(int i = 0; i < worsenings; i++) + removeMagicEffect(target, spellParams, otherEffect); + } + } + } + //Note that we remove the effects, but keep the params + target.getClass().getCreatureStats(target).getActiveSpells().purge([&spellParams] (const ActiveSpells::ActiveSpellParams& params, const auto&) + { + return &spellParams == ¶ms; + }, target); + } + break; + } +} + +void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return; + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMagnitude)); + removeMagicEffect(target, spellParams, effect); + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); +} + +} \ No newline at end of file diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp new file mode 100644 index 0000000000..a9e7b066f3 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -0,0 +1,20 @@ +#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H +#define GAME_MWMECHANICS_SPELLEFFECTS_H + +#include "activespells.hpp" + +#include "../mwworld/ptr.hpp" + +// These functions should probably be split up into separate Lua functions for each magic effect when magic is dehardcoded. +// That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion. + +namespace MWMechanics +{ + // Applies a tick of a single effect. Returns true if the effect should be removed immediately + bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + + // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce + void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); +} + +#endif diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index 5920949d65..f5759fd7ee 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -16,12 +16,6 @@ namespace ESM namespace MWMechanics { - struct SpellParams - { - std::map mEffectRands; // - std::set mPurgedEffects; // indices of purged effects - }; - class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index bd9c5f7cb3..974d297f10 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -31,21 +31,20 @@ namespace // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. if (effectFilter == -1) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->getId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - int effectId = effectIt->mEffectId; + int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway + if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) @@ -64,15 +63,14 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->first != spellId) + if (it->getId() != spellId) continue; - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - if (effectIt->mDuration > duration) - duration = effectIt->mDuration; + if (effect.mDuration > duration) + duration = effect.mDuration; } } return duration; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index b871376003..6520ae3ab3 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -20,72 +20,33 @@ namespace MWMechanics { Spells::Spells() - : mSpellsChanged(false) { } Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), - mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers), - mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects) + mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers) { if(mSpellList) mSpellList->addListener(this); } Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), - mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), - mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), - mSourcedEffects(std::move(spells.mSourcedEffects)) + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)) { if (mSpellList) mSpellList->updateListener(&spells, this); } - std::map::const_iterator Spells::begin() const + std::vector::const_iterator Spells::begin() const { return mSpells.begin(); } - std::map::const_iterator Spells::end() const + std::vector::const_iterator Spells::end() const { return mSpells.end(); } - void Spells::rebuildEffects() const - { - mEffects = MagicEffects(); - mSourcedEffects.clear(); - - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - - if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || - spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) - { - int i=0; - for (const auto& effect : spell->mEffects.mList) - { - if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) - { - ++i; - continue; // effect was purged - } - - float random = 1.f; - if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) - random = iter.second.mEffectRands.at(i); - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; - mEffects.add (effect, magnitude); - mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); - - ++i; - } - } - } - } - bool Spells::hasSpell(const std::string &spell) const { return hasSpell(SpellList::getSpell(spell)); @@ -93,7 +54,7 @@ namespace MWMechanics bool Spells::hasSpell(const ESM::Spell *spell) const { - return mSpells.find(spell) != mSpells.end(); + return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) @@ -108,29 +69,8 @@ namespace MWMechanics void Spells::addSpell(const ESM::Spell* spell) { - if (mSpells.find (spell)==mSpells.end()) - { - std::map random; - - // Determine the random magnitudes (unless this is a castable spell, in which case - // they will be determined when the spell is cast) - if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) - { - for (unsigned int i=0; imEffects.mList.size();++i) - { - if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) - { - int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; - random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - } - } - } - - SpellParams params; - params.mEffectRands = random; - mSpells.emplace(spell, params); - mSpellsChanged = true; - } + if (!hasSpell(spell)) + mSpells.emplace_back(spell); } void Spells::remove (const std::string& spellId) @@ -145,27 +85,14 @@ namespace MWMechanics void Spells::removeSpell(const ESM::Spell* spell) { - const auto it = mSpells.find(spell); + const auto it = std::find(mSpells.begin(), mSpells.end(), spell); if(it != mSpells.end()) - { mSpells.erase(it); - mSpellsChanged = true; - } - } - - MagicEffects Spells::getMagicEffects() const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); - mSpellsChanged = true; } void Spells::clear(bool modifyBase) @@ -185,26 +112,10 @@ namespace MWMechanics return mSelectedSpell; } - bool Spells::isSpellActive(const std::string &id) const - { - if (id.empty()) - return false; - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if (spell && hasSpell(spell)) - { - auto type = spell->mData.mType; - return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); - } - - return false; - } - bool Spells::hasDisease(const ESM::Spell::SpellType type) const { - for (const auto& iter : mSpells) + for (const auto spell : mSpells) { - const ESM::Spell *spell = iter.first; if (spell->mData.mType == type) return true; } @@ -227,12 +138,11 @@ namespace MWMechanics std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { - const ESM::Spell *spell = iter->first; + const ESM::Spell *spell = *iter; if (filter(spell)) { - mSpells.erase(iter++); + iter = mSpells.erase(iter); purged.push_back(spell->mId); - mSpellsChanged = true; } else ++iter; @@ -261,43 +171,6 @@ namespace MWMechanics purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } - void Spells::removeEffects(const std::string &id) - { - if (isSpellActive(id)) - { - for (auto& spell : mSpells) - { - if (spell.first == SpellList::getSpell(id)) - { - for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) - { - spell.second.mPurgedEffects.insert(i); - } - } - } - - mSpellsChanged = true; - } - } - - void Spells::visitEffectSources(EffectSourceVisitor &visitor) const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - - for (const auto& it : mSourcedEffects) - { - const ESM::Spell * spell = it.first; - for (const auto& effectIt : it.second) - { - // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here - visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); - } - } - } - bool Spells::hasCorprusEffect(const ESM::Spell *spell) { for (const auto& effectIt : spell->mEffects.mList) @@ -310,46 +183,6 @@ namespace MWMechanics return false; } - void Spells::purgeEffect(int effectId) - { - for (auto& spellIt : mSpells) - { - int i = 0; - for (auto& effectIt : spellIt.first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt.second.mPurgedEffects.insert(i); - mSpellsChanged = true; - } - ++i; - } - } - } - - void Spells::purgeEffect(int effectId, const std::string & sourceId) - { - // Effect source may be not a spell - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); - if (spell == nullptr) - return; - - auto spellIt = mSpells.find(spell); - if (spellIt == mSpells.end()) - return; - - int index = 0; - for (auto& effectIt : spellIt->first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt->second.mPurgedEffects.insert(index); - mSpellsChanged = true; - } - ++index; - } - } - bool Spells::canUsePower(const ESM::Spell* spell) const { const auto it = mUsedPowers.find(spell); @@ -365,17 +198,16 @@ namespace MWMechanics { const auto& baseSpells = mSpellList->getSpells(); - for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const std::string& id : state.mSpells) { // Discard spells that are no longer available due to changed content files - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell) { - mSpells[spell].mEffectRands = it->second.mEffectRands; - mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; + addSpell(spell); - if (it->first == state.mSelectedSpell) - mSelectedSpell = it->first; + if (id == state.mSelectedSpell) + mSelectedSpell = id; } } // Add spells from the base record @@ -394,31 +226,6 @@ namespace MWMechanics mUsedPowers[spell] = MWWorld::TimeStamp(it->second); } - for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); - if (!spell) - continue; - - CorprusStats stats; - - int worsening = state.mCorprusSpells.at(it->first).mWorsenings; - - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = worsening; - } - stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - - creatureStats->addCorprusSpell(it->first, stats); - } - - mSpellsChanged = true; - // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. for (std::map >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) @@ -457,16 +264,13 @@ namespace MWMechanics void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); - for (const auto& it : mSpells) + for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record - if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || - std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) + if((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) || + std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end()) { - ESM::SpellState::SpellParams params; - params.mEffectRands = it.second.mEffectRands; - params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.emplace(it.first->mId, params); + state.mSpells.emplace_back(spell->mId); } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 9737b72cd0..29f505d369 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -29,18 +29,13 @@ namespace MWMechanics class Spells { std::shared_ptr mSpellList; - std::map mSpells; + std::vector mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; - mutable bool mSpellsChanged; - mutable MagicEffects mEffects; - mutable std::map mSourcedEffects; - void rebuildEffects() const; - bool hasDisease(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); @@ -52,8 +47,6 @@ namespace MWMechanics friend class SpellList; public: - using TIterator = std::map::const_iterator; - Spells(); Spells(const Spells&); @@ -64,9 +57,6 @@ namespace MWMechanics static bool hasCorprusEffect(const ESM::Spell *spell); - void purgeEffect(int effectId); - void purgeEffect(int effectId, const std::string & sourceId); - bool canUsePower (const ESM::Spell* spell) const; void usePower (const ESM::Spell* spell); @@ -75,9 +65,9 @@ namespace MWMechanics void purgeCorprusDisease(); void purgeCurses(); - TIterator begin() const; + std::vector::const_iterator begin() const; - TIterator end() const; + std::vector::const_iterator end() const; bool hasSpell(const std::string& spell) const; bool hasSpell(const ESM::Spell* spell) const; @@ -92,9 +82,6 @@ namespace MWMechanics ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). - MagicEffects getMagicEffects() const; - ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. - void clear(bool modifyBase = false); ///< Remove all spells of al types. @@ -104,17 +91,10 @@ namespace MWMechanics const std::string getSelectedSpell() const; ///< May return an empty string. - bool isSpellActive(const std::string& id) const; - ///< Are we under the effects of the given spell ID? - bool hasCommonDisease() const; bool hasBlightDisease() const; - void removeEffects(const std::string& id); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - void readState (const ESM::SpellState& state, CreatureStats* creatureStats); void writeState (ESM::SpellState& state) const; diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index d90545fc60..c1923d4337 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -60,90 +60,60 @@ namespace MWMechanics return std::string(); } - UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) - : mActor(actor) + int summonCreature(int effectId, const MWWorld::Ptr& summoner) { - } - - void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) - { - if (isSummoningEffect(key.mId) && magnitude > 0) + std::string creatureID = getSummonedCreature(effectId); + int creatureActorId = -1; + if (!creatureID.empty()) { - mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); - } - } - - void UpdateSummonedCreatures::process(bool cleanup) - { - MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - - for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) - { - bool found = creatureMap.find(*it) != creatureMap.end(); - if (!found) + try { - std::string creatureID = getSummonedCreature(it->mEffectId); - if (!creatureID.empty()) + auto world = MWBase::Environment::get().getWorld(); + MWWorld::ManualRef ref(world->getStore(), creatureID, 1); + + MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); + + // Make the summoned creature follow its master and help in fights + AiFollow package(summoner); + summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + creatureActorId = summonedCreatureStats.getActorId(); + + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); + + MWRender::Animation* anim = world->getAnimation(placed); + if (anim) { - int creatureActorId = -1; - try - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); - - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - - // Make the summoned creature follow its master and help in fights - AiFollow package(mActor); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - creatureActorId = summonedCreatureStats.getActorId(); - - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); - - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); - // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log - } - - creatureMap.emplace(*it, creatureActorId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_Start"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1, false); } } - } - - // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) - { - bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); - if (!found) + catch (std::exception& e) { - // Effect has ended - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); - creatureMap.erase(it++); - continue; + Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); + // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } - ++it; + + summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId); } + return creatureActorId; + } + + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup) + { + MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature); if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (auto it = creatureMap.begin(); it != creatureMap.end(); ) { if(it->second == -1) { @@ -155,21 +125,22 @@ namespace MWMechanics if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - purgeSummonEffect(mActor, *it); + auto summon = *it; creatureMap.erase(it++); + purgeSummonEffect(summoner, summon); } else ++it; } } - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); - creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex); - creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId); - if (summoner.getClass().hasInventoryStore(summoner)) - summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex); + creatureStats.getActiveSpells().purge([summon] (const auto& spell, const auto& effect) + { + return effect.mEffectId == summon.first && effect.mArg == summon.second; + }, summoner); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); } diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index 3c3e18a96b..3186eef986 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -11,32 +11,15 @@ namespace MWMechanics { - class CreatureStats; - bool isSummoningEffect(int effectId); std::string getSummonedCreature(int effectId); - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - struct UpdateSummonedCreatures : public EffectSourceVisitor - { - UpdateSummonedCreatures(const MWWorld::Ptr& actor); - virtual ~UpdateSummonedCreatures() = default; - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - - /// To call after all effect sources have been visited - void process(bool cleanup); - - private: - MWWorld::Ptr mActor; - - std::set mActiveEffects; - }; + int summonCreature(int effectId, const MWWorld::Ptr& summoner); + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp deleted file mode 100644 index 5056179f8f..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "tickableeffects.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "actorutil.hpp" -#include "npcstats.hpp" - -namespace MWMechanics -{ - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) - { - if (!ptr.getClass().hasInventoryStore(ptr)) - return false; - - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - - return false; - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - if (godmode) - break; - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - if (godmode) - break; - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - if (godmode) - break; - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (!godmode || magnitude <= 0) - { - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - } - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - if (!godmode || magnitude <= 0) - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - if (godmode) - break; - static const std::array priorities - { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - for (const int priority : priorities) - { - if (disintegrateSlot(actor, priority, magnitude)) - break; - } - - break; - } - case ESM::MagicEffect::DisintegrateWeapon: - if (!godmode) - disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); - break; - - case ESM::MagicEffect::SunDamage: - { - // isInCell shouldn't be needed, but updateActor called during game start - if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - if (godmode) - break; - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } -} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp deleted file mode 100644 index ccd42ca19b..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MWMECHANICS_TICKABLEEFFECTS_H -#define MWMECHANICS_TICKABLEEFFECTS_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class CreatureStats; - struct EffectKey; - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); -} - -#endif diff --git a/apps/openmw/mwphysics/collisiontype.hpp b/apps/openmw/mwphysics/collisiontype.hpp index 0d6a32fc09..e69534cf68 100644 --- a/apps/openmw/mwphysics/collisiontype.hpp +++ b/apps/openmw/mwphysics/collisiontype.hpp @@ -10,7 +10,8 @@ enum CollisionType { CollisionType_Actor = 1<<2, CollisionType_HeightMap = 1<<3, CollisionType_Projectile = 1<<4, - CollisionType_Water = 1<<5 + CollisionType_Water = 1<<5, + CollisionType_Default = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door }; } diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index 275325cf67..fc8725f5f4 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -22,28 +22,36 @@ namespace MWPhysics return nearest.distance(position) < radius; } + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object, - const int mask, const int group) + const int mask, const int group, OnCollision* onCollision) : mPosition(position), mRadius(radius), mCollisionObject(object), mCollisionFilterMask(mask), - mCollisionFilterGroup(group) + mCollisionFilterGroup(group), + mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult) + if (mResult && mOnCollision == nullptr) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (collisionObject == mCollisionObject) + if (collisionObject == mCollisionObject + || !needsCollision(*proxy) + || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; - if (needsCollision(*proxy)) - mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius); + mResult = true; + if (mOnCollision != nullptr) + { + (*mOnCollision)(collisionObject); + return true; + } return !mResult; } @@ -58,6 +66,7 @@ namespace MWPhysics btCollisionObject* mCollisionObject; int mCollisionFilterMask; int mCollisionFilterGroup; + OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 7363b9964a..a0dde67c2e 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -22,31 +22,82 @@ namespace { - /// @brief A scoped lock that is either shared or exclusive depending on configuration + /// @brief A scoped lock that is either exclusive or inexistent depending on configuration + template + class MaybeExclusiveLock + { + public: + /// @param mutex a mutex + /// @param threadCount decide wether the excluse lock will be taken + MaybeExclusiveLock(Mutex& mutex, int threadCount) : mMutex(mutex), mThreadCount(threadCount) + { + assert(threadCount >= 0); + if (mThreadCount > 0) + mMutex.lock(); + } + + ~MaybeExclusiveLock() + { + if (mThreadCount > 0) + mMutex.unlock(); + } + + private: + Mutex& mMutex; + unsigned int mThreadCount; + }; + + /// @brief A scoped lock that is either shared or inexistent depending on configuration template class MaybeSharedLock { public: /// @param mutex a shared mutex - /// @param canBeSharedLock decide wether the lock will be shared or exclusive - MaybeSharedLock(Mutex& mutex, bool canBeSharedLock) : mMutex(mutex), mCanBeSharedLock(canBeSharedLock) + /// @param threadCount decide wether the shared lock will be taken + MaybeSharedLock(Mutex& mutex, int threadCount) : mMutex(mutex), mThreadCount(threadCount) { - if (mCanBeSharedLock) + assert(threadCount >= 0); + if (mThreadCount > 0) mMutex.lock_shared(); - else - mMutex.lock(); } ~MaybeSharedLock() { - if (mCanBeSharedLock) + if (mThreadCount > 0) mMutex.unlock_shared(); - else + } + + private: + Mutex& mMutex; + unsigned int mThreadCount; + }; + + /// @brief A scoped lock that is either shared, exclusive or inexistent depending on configuration + template + class MaybeLock + { + public: + /// @param mutex a shared mutex + /// @param threadCount decide wether the lock will be shared, exclusive or inexistent + MaybeLock(Mutex& mutex, int threadCount) : mMutex(mutex), mThreadCount(threadCount) + { + assert(threadCount >= 0); + if (mThreadCount > 1) + mMutex.lock_shared(); + else if(mThreadCount == 1) + mMutex.lock(); + } + + ~MaybeLock() + { + if (mThreadCount > 1) + mMutex.unlock_shared(); + else if(mThreadCount == 1) mMutex.unlock(); } private: Mutex& mMutex; - bool mCanBeSharedLock; + unsigned int mThreadCount; }; bool isUnderWater(const MWPhysics::ActorFrameData& actorData) @@ -54,29 +105,6 @@ namespace return actorData.mPosition.z() < actorData.mSwimLevel; } - void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed) - { - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); - - if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) - actorData.mNeedLand = true; - else if (heightDiff < 0) - actorData.mFallHeight += heightDiff; - } - - void updateMechanics(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData) - { - auto ptr = actor.getPtr(); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - if (actorData.mNeedLand) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); - else if (actorData.mFallHeight < 0) - stats.addToFallHeight(-actorData.mFallHeight); - } - osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); @@ -85,14 +113,14 @@ namespace namespace Config { - /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading - int computeNumThreads(bool& threadSafeBullet) + /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading and user requested more than 1 background threads + int computeNumThreads() { int wantedThread = Settings::Manager::getInt("async num threads", "Physics"); auto broad = std::make_unique(); auto maxSupportedThreads = broad->m_rayTestStacks.size(); - threadSafeBullet = (maxSupportedThreads > 1); + auto threadSafeBullet = (maxSupportedThreads > 1); if (!threadSafeBullet && wantedThread > 1) { Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used"; @@ -111,6 +139,7 @@ namespace MWPhysics , mTimeAccum(0.f) , mCollisionWorld(collisionWorld) , mDebugDrawer(debugDrawer) + , mNumThreads(Config::computeNumThreads()) , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) @@ -130,8 +159,6 @@ namespace MWPhysics , mTimeEnd(0) , mFrameStart(0) { - mNumThreads = Config::computeNumThreads(mThreadSafeBullet); - if (mNumThreads >= 1) { for (int i = 0; i < mNumThreads; ++i) @@ -151,11 +178,12 @@ namespace MWPhysics PhysicsTaskScheduler::~PhysicsTaskScheduler() { - std::unique_lock lock(mSimulationMutex); - mQuit = true; - mNumJobs = 0; - mRemainingSteps = 0; - lock.unlock(); + { + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); + mQuit = true; + mNumJobs = 0; + mRemainingSteps = 0; + } mHasJob.notify_all(); for (auto& thread : mThreads) thread.join(); @@ -212,7 +240,7 @@ namespace MWPhysics // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. - std::unique_lock lock(mSimulationMutex); + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); assert(actors.size() == actorsData.size()); double timeStart = mTimer->tick(); @@ -220,11 +248,8 @@ namespace MWPhysics // start by finishing previous background computation if (mNumThreads != 0) { - for (size_t i = 0; i < mActors.size(); ++i) - { - updateMechanics(*mActors[i], mActorsFrameData[i]); - updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); - } + syncWithMainThread(); + if(mAdvanceSimulation) mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); updateStats(frameStart, frameNumber, stats); @@ -258,14 +283,14 @@ namespace MWPhysics if (mNumThreads == 0) { - syncComputation(); + doSimulation(); + syncWithMainThread(); if(mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); return; } mAsyncStartTime = mTimer->tick(); - lock.unlock(); mHasJob.notify_all(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); @@ -273,7 +298,7 @@ namespace MWPhysics void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { - std::unique_lock lock(mSimulationMutex); + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); mActors.clear(); @@ -287,25 +312,25 @@ namespace MWPhysics void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->rayTest(rayFromWorld, rayToWorld, resultCallback); } void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback); } void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); // target the collision object's world origin, this should be the center of the collision object btTransform rayTo; rayTo.setIdentity(); @@ -322,33 +347,33 @@ namespace MWPhysics void PhysicsTaskScheduler::aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback); } void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max); } void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask; } void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { mCollisionObjects.insert(collisionObject); - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { mCollisionObjects.erase(collisionObject); - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->removeCollisionObject(collisionObject); } @@ -360,14 +385,14 @@ namespace MWPhysics } else { - std::unique_lock lock(mUpdateAabbMutex); + MaybeExclusiveLock lock(mUpdateAabbMutex, mNumThreads); mUpdateAabb.insert(std::move(ptr)); } } bool PhysicsTaskScheduler::getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2) { - std::unique_lock lock(mLOSCacheMutex); + MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads); auto req = LOSRequest(actor1, actor2); auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req); @@ -383,7 +408,7 @@ namespace MWPhysics void PhysicsTaskScheduler::refreshLOSCache() { - std::shared_lock lock(mLOSCacheMutex); + MaybeSharedLock lock(mLOSCacheMutex, mNumThreads); int job = 0; int numLOS = mLOSCache.size(); while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS) @@ -402,7 +427,7 @@ namespace MWPhysics void PhysicsTaskScheduler::updateAabbs() { - std::scoped_lock lock(mUpdateAabbMutex); + MaybeExclusiveLock lock(mUpdateAabbMutex, mNumThreads); std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), [this](const std::shared_ptr& ptr) { updatePtrAabb(ptr); }); mUpdateAabb.clear(); @@ -410,7 +435,7 @@ namespace MWPhysics void PhysicsTaskScheduler::updatePtrAabb(const std::shared_ptr& ptr) { - std::scoped_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); if (const auto actor = std::dynamic_pointer_cast(ptr)) { actor->updateCollisionObjectPosition(); @@ -436,27 +461,7 @@ namespace MWPhysics if (!mNewFrame) mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); - mPreStepBarrier->wait([this] { afterPreStep(); }); - - int job = 0; - while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) - { - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); - } - - mPostStepBarrier->wait([this] { afterPostStep(); }); - - if (!mRemainingSteps) - { - while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) - { - handleFall(mActorsFrameData[job], mAdvanceSimulation); - } - - refreshLOSCache(); - mPostSimBarrier->wait([this] { afterPostSim(); }); - } + doSimulation(); } } @@ -466,7 +471,7 @@ namespace MWPhysics { if (mActors[i]->setPosition(mActorsFrameData[i].mPosition)) { - std::scoped_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); mActorsFrameData[i].mPosition = mActors[i]->getPosition(); // account for potential position change made by script mActors[i]->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(mActors[i]->getCollisionObject()); @@ -476,6 +481,17 @@ namespace MWPhysics void PhysicsTaskScheduler::updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const { + auto ptr = actor.getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; + const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); + + if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt)); actor.setLastStuckPosition(actorData.mLastStuckPosition); actor.setStuckFrames(actorData.mStuckFrames); @@ -504,32 +520,29 @@ namespace MWPhysics resultCallback.m_collisionFilterGroup = 0xFF; resultCallback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); mCollisionWorld->rayTest(pos1, pos2, resultCallback); return !resultCallback.hasHit(); } - void PhysicsTaskScheduler::syncComputation() + void PhysicsTaskScheduler::doSimulation() { - while (mRemainingSteps--) + while (mRemainingSteps) { - for (auto& actorData : mActorsFrameData) + mPreStepBarrier->wait([this] { afterPreStep(); }); + int job = 0; + while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { - MovementSolver::unstuck(actorData, mCollisionWorld); - MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld, *mWorldFrameData); + MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); + MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); } - updateActorsPositions(); + mPostStepBarrier->wait([this] { afterPostStep(); }); } - for (size_t i = 0; i < mActors.size(); ++i) - { - handleFall(mActorsFrameData[i], mAdvanceSimulation); - updateMechanics(*mActors[i], mActorsFrameData[i]); - updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); - } refreshLOSCache(); + mPostSimBarrier->wait([this] { afterPostSim(); }); } void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -549,7 +562,7 @@ namespace MWPhysics void PhysicsTaskScheduler::debugDraw() { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); mDebugDrawer->step(); } @@ -575,7 +588,7 @@ namespace MWPhysics return; for (size_t i = 0; i < mActors.size(); ++i) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); } } @@ -594,7 +607,7 @@ namespace MWPhysics { mNewFrame = false; { - std::unique_lock lock(mLOSCacheMutex); + MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads); mLOSCache.erase( std::remove_if(mLOSCache.begin(), mLOSCache.end(), [](const LOSRequest& req) { return req.mStale; }), @@ -602,4 +615,10 @@ namespace MWPhysics } mTimeEnd = mTimer->tick(); } + + void PhysicsTaskScheduler::syncWithMainThread() + { + for (size_t i = 0; i < mActors.size(); ++i) + updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); + } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index e2cfccffb0..08997947e4 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -61,7 +61,7 @@ namespace MWPhysics void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: - void syncComputation(); + void doSimulation(); void worker(); void updateActorsPositions(); void updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const; @@ -74,6 +74,7 @@ namespace MWPhysics void afterPreStep(); void afterPostStep(); void afterPostSim(); + void syncWithMainThread(); std::unique_ptr mWorldFrameData; std::vector> mActors; @@ -98,7 +99,6 @@ namespace MWPhysics int mLOSCacheExpiry; bool mNewFrame; bool mAdvanceSimulation; - bool mThreadSafeBullet; bool mQuit; std::atomic mNextJob; std::atomic mNextLOS; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index b3c4ef6168..6b2600069a 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -348,11 +348,11 @@ namespace MWPhysics return result; } - RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius) const + RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius, int mask, int group) const { btCollisionWorld::ClosestConvexResultCallback callback(Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); - callback.m_collisionFilterGroup = 0xff; - callback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; + callback.m_collisionFilterGroup = group; + callback.m_collisionFilterMask = mask; btSphereShape shape(radius); const btQuaternion btrot = btQuaternion::getIdentity(); @@ -368,6 +368,8 @@ namespace MWPhysics { result.mHitPos = Misc::Convert::toOsg(callback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(callback.m_hitNormalWorld); + if (auto* ptrHolder = static_cast(callback.m_hitCollisionObject->getUserPointer())) + result.mHitObject = ptrHolder->getPtr(); } return result; } @@ -490,7 +492,7 @@ namespace MWPhysics return heightField->second.get(); } - void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType, bool skipAnimated) + void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType) { if (ptr.mRef->mData.mPhysicsPostponed) return; @@ -498,9 +500,6 @@ namespace MWPhysics if (!shapeInstance || !shapeInstance->getCollisionShape()) return; - if (skipAnimated && shapeInstance->isAnimated()) - return; - assert(!getObject(ptr)); auto obj = std::make_shared(ptr, shapeInstance, rotation, collisionType, mTaskScheduler.get()); @@ -512,18 +511,18 @@ namespace MWPhysics void PhysicsSystem::remove(const MWWorld::Ptr &ptr) { - if (auto found = mObjects.find(ptr.mRef); found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { if (mUnrefQueue.get()) - mUnrefQueue->push(found->second->getShapeInstance()); + mUnrefQueue->push(foundObject->second->getShapeInstance()); - mAnimatedObjects.erase(found->second.get()); + mAnimatedObjects.erase(foundObject->second.get()); - mObjects.erase(found); + mObjects.erase(foundObject); } - else if (auto found = mActors.find(ptr.mRef); found != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { - mActors.erase(found); + mActors.erase(foundActor); } } @@ -589,16 +588,16 @@ namespace MWPhysics void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) { - if (auto found = mObjects.find(ptr.mRef); found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { float scale = ptr.getCellRef().getScale(); - found->second->setScale(scale); - mTaskScheduler->updateSingleAabb(found->second); + foundObject->second->setScale(scale); + mTaskScheduler->updateSingleAabb(foundObject->second); } - else if (auto found = mActors.find(ptr.mRef); found != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { - found->second->updateScale(); - mTaskScheduler->updateSingleAabb(found->second); + foundActor->second->updateScale(); + mTaskScheduler->updateSingleAabb(foundActor->second); } } @@ -624,39 +623,39 @@ namespace MWPhysics mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(resultCallback.m_hitPointWorld); + const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition()); projectile->setPosition(newpos); mTaskScheduler->updateSingleAabb(foundProjectile->second); } void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate) { - if (auto found = mObjects.find(ptr.mRef); found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setRotation(rotate); - mTaskScheduler->updateSingleAabb(found->second); + foundObject->second->setRotation(rotate); + mTaskScheduler->updateSingleAabb(foundObject->second); } - else if (auto found = mActors.find(ptr.mRef); found != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { - if (!found->second->isRotationallyInvariant()) + if (!foundActor->second->isRotationallyInvariant()) { - found->second->setRotation(rotate); - mTaskScheduler->updateSingleAabb(found->second); + foundActor->second->setRotation(rotate); + mTaskScheduler->updateSingleAabb(foundActor->second); } } } void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr) { - if (auto found = mObjects.find(ptr.mRef); found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->updatePosition(); - mTaskScheduler->updateSingleAabb(found->second); + foundObject->second->updatePosition(); + mTaskScheduler->updateSingleAabb(foundObject->second); } - else if (auto found = mActors.find(ptr.mRef); found != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { - found->second->updatePosition(); - mTaskScheduler->updateSingleAabb(found->second, true); + foundActor->second->updatePosition(); + mTaskScheduler->updateSingleAabb(foundActor->second, true); } } @@ -686,7 +685,7 @@ namespace MWPhysics mActors.emplace(ptr.mRef, std::move(actor)); } - int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater) + int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); @@ -694,7 +693,7 @@ namespace MWPhysics mProjectileId++; - auto projectile = std::make_shared(caster, position, radius, canTraverseWater, mTaskScheduler.get(), this); + auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); mProjectiles.emplace(mProjectileId, std::move(projectile)); return mProjectileId; @@ -921,7 +920,8 @@ namespace MWPhysics CollisionType_Actor|CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const { btCollisionObject* object = nullptr; const auto it = mActors.find(ignore.mRef); @@ -932,7 +932,19 @@ namespace MWPhysics const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = 0xff; - HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group); + if (occupyingActors == nullptr) + { + HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group, + static_cast(nullptr)); + mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); + return callback.getResult(); + } + const auto onCollision = [&] (const btCollisionObject* object) + { + if (PtrHolder* holder = static_cast(object->getUserPointer())) + occupyingActors->push_back(holder->getPtr()); + }; + HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group, &onCollision); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } @@ -966,14 +978,12 @@ namespace MWPhysics , mWaterlevel(waterlevel) , mHalfExtentsZ(actor.getHalfExtents().z()) , mOldHeight(0) - , mFallHeight(0) , mStuckFrames(0) , mFlying(MWBase::Environment::get().getWorld()->isFlying(actor.getPtr())) , mWasOnGround(actor.getOnGround()) , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mWaterCollision(waterCollision) , mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode()) - , mNeedLand(false) { } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 78569bb0c3..cf4ca40a54 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -97,14 +97,12 @@ namespace MWPhysics const float mWaterlevel; const float mHalfExtentsZ; float mOldHeight; - float mFallHeight; unsigned int mStuckFrames; const bool mFlying; const bool mWasOnGround; const bool mIsAquatic; const bool mWaterCollision; const bool mSkipCollisionDetection; - bool mNeedLand; }; struct WorldFrameData @@ -128,10 +126,10 @@ namespace MWPhysics void setWaterHeight(float height); void disableWater(); - void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType = CollisionType_World, bool skipAnimated = false); + void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType = CollisionType_World); void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); - int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater); + int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); void updateProjectile(const int projectileId, const osg::Vec3f &position) const; void removeProjectile(const int projectileId); @@ -186,9 +184,10 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), const std::vector& targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; + int mask = CollisionType_Default, int group=0xff) const override; - RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; + RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const override; /// Return true if actor1 can see actor2. bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; @@ -251,7 +250,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index a8bb444956..4efb245149 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -15,12 +15,10 @@ namespace MWPhysics { -Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) - : mCanCrossWaterSurface(canCrossWaterSurface) - , mCrossedWaterSurface(false) +Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) + : mHitWater(false) , mActive(true) , mHitTarget(nullptr) - , mWaterHitPosition(std::nullopt) , mPhysics(physicssystem) , mTaskScheduler(scheduler) { @@ -75,11 +73,6 @@ osg::Vec3f Projectile::getPosition() const return mPosition; } -bool Projectile::canTraverseWater() const -{ - return mCanCrossWaterSurface; -} - void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) { bool active = true; @@ -143,17 +136,4 @@ bool Projectile::isValidTarget(const btCollisionObject* target) const [target](const btCollisionObject* actor) { return target == actor; }); } -std::optional Projectile::getWaterHitPosition() -{ - return std::exchange(mWaterHitPosition, std::nullopt); -} - -void Projectile::setWaterHitPosition(btVector3 pos) -{ - if (mCrossedWaterSurface) - return; - mCrossedWaterSurface = true; - mWaterHitPosition = pos; -} - } diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index dd659b6581..5e4e487c03 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include @@ -32,7 +31,7 @@ namespace MWPhysics class Projectile final : public PtrHolder { public: - Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); + Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); ~Projectile() override; btConvexShape* getConvexShape() const { return mConvexShape; } @@ -56,15 +55,25 @@ namespace MWPhysics return mCasterColObj; } - bool canTraverseWater() const; + void setHitWater() + { + mHitWater = true; + } + + bool getHitWater() const + { + return mHitWater; + } void hit(const btCollisionObject* target, btVector3 pos, btVector3 normal); void setValidTargets(const std::vector& targets); bool isValidTarget(const btCollisionObject* target) const; - std::optional getWaterHitPosition(); - void setWaterHitPosition(btVector3 pos); + btVector3 getHitPosition() const + { + return mHitPosition; + } private: @@ -72,13 +81,11 @@ namespace MWPhysics btConvexShape* mConvexShape; bool mTransformUpdatePending; - bool mCanCrossWaterSurface; - bool mCrossedWaterSurface; + bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; const btCollisionObject* mCasterColObj; const btCollisionObject* mHitTarget; - std::optional mWaterHitPosition; osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; diff --git a/apps/openmw/mwphysics/projectileconvexcallback.cpp b/apps/openmw/mwphysics/projectileconvexcallback.cpp index 687253e1cc..6520be787d 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.cpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.cpp @@ -49,9 +49,7 @@ namespace MWPhysics } case CollisionType_Water: { - mProjectile->setWaterHitPosition(m_hitPointWorld); - if (mProjectile->canTraverseWater()) - return 1.f; + mProjectile->setHitWater(); break; } } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index e13e745fec..d00f23e2c4 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -29,9 +29,10 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. virtual RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), const std::vector& targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; + int mask = CollisionType_Default, int group=0xff) const = 0; - virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; + virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const = 0; /// Return true if actor1 can see actor2. virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index cb46354590..f556e6891a 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -67,14 +67,17 @@ ActorAnimation::~ActorAnimation() PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor) { + osg::Group* parent = getBoneByName(bonename); + if (!parent) + return nullptr; + + osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); + const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) return PartHolderPtr(); - osg::Group* parent = found->second; - osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); - if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); @@ -133,9 +136,9 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const MWMechanics::CreatureStats &stats = cls.getCreatureStats(mPtr); if (cls.hasInventoryStore(mPtr) && weaptype != ESM::Weapon::Spell) { - osg::Group* foundNode = getBoneByName ("Bip01 AttachShield"); - - if (foundNode || (mPtr == MWMechanics::getPlayer() && mPtr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson())) + SceneUtil::FindByNameVisitor findVisitor ("Bip01 AttachShield"); + mObjectRoot->accept(findVisitor); + if (findVisitor.mFoundNode || (mPtr == MWMechanics::getPlayer() && mPtr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson())) { const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); @@ -273,11 +276,10 @@ osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) const if (!mObjectRoot) return nullptr; - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(boneName)); - if (found == nodeMap.end()) - return nullptr; - return found->second; + SceneUtil::FindByNameVisitor findVisitor (boneName); + mObjectRoot->accept(findVisitor); + + return findVisitor.mFoundNode; } std::string ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index a04724c70a..475c656cc9 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -86,22 +87,22 @@ namespace std::vector > mToRemove; }; - class DayNightCallback : public osg::NodeCallback + class DayNightCallback : public SceneUtil::NodeCallback { public: DayNightCallback() : mCurrentState(0) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Switch* node, osg::NodeVisitor* nv) { unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode(); - const unsigned int newState = node->asGroup()->getNumChildren() > state ? state : 0; + const unsigned int newState = node->getNumChildren() > state ? state : 0; if (newState != mCurrentState) { mCurrentState = newState; - node->asSwitch()->setSingleChildOn(mCurrentState); + node->setSingleChildOn(mCurrentState); } traverse(node, nv); @@ -471,20 +472,18 @@ namespace MWRender } } - class ResetAccumRootCallback : public osg::NodeCallback + class ResetAccumRootCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::MatrixTransform* transform, osg::NodeVisitor* nv) { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix mat = transform->getMatrix(); osg::Vec3f position = mat.getTrans(); position = osg::componentMultiply(mResetAxes, position); mat.setTrans(position); transform->setMatrix(mat); - traverse(node, nv); + traverse(transform, nv); } void setAccumulate(const osg::Vec3f& accumulate) @@ -591,8 +590,6 @@ namespace MWRender void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -600,21 +597,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-3, 3, "/"); - mResourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) - addSingleAnimSource(name, baseModel); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "kf") + addSingleAnimSource(name, baseModel); } } @@ -1295,8 +1281,6 @@ namespace MWRender if (model.empty()) return; - const std::map& index = resourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -1304,21 +1288,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-4, 4, "/"); - resourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".nif") == 0) - loadBonesFromFile(node, name, resourceSystem); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "nif") + loadBonesFromFile(node, name, resourceSystem); } } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index ffcd9b3260..84788c1e2d 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -245,7 +246,7 @@ protected: // Keep track of controllers that we added to our scene graph. // We may need to rebuild these controllers when the active animation groups / sources change. - std::vector, osg::ref_ptr>> mActiveControllers; + std::vector, osg::ref_ptr>> mActiveControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; @@ -506,7 +507,7 @@ public: bool canBeHarvested() const override; }; -class UpdateVfxCallback : public osg::NodeCallback +class UpdateVfxCallback : public SceneUtil::NodeCallback { public: UpdateVfxCallback(EffectParams& params) @@ -519,7 +520,7 @@ public: bool mFinished; EffectParams mParams; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osg::NodeVisitor* nv); private: double mStartingTime; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 3e5d1d0b31..e750fcad46 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -25,7 +26,7 @@ namespace { -class UpdateRenderCameraCallback : public osg::NodeCallback +class UpdateRenderCameraCallback : public SceneUtil::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) @@ -33,12 +34,10 @@ public: { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* cam, osg::NodeVisitor* nv) { - osg::Camera* cam = static_cast(node); - // traverse first to update animations, in case the camera is attached to an animated node - traverse(node, nv); + traverse(cam, nv); mCamera->updateCamera(cam); } @@ -251,6 +250,7 @@ namespace MWRender const float cameraObstacleLimit = 5.0f; const float focalObstacleLimit = 10.f; + const int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor); const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); @@ -260,7 +260,7 @@ namespace MWRender float offsetLen = focalOffset.length(); if (offsetLen > 0) { - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit, collisionType); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; @@ -274,7 +274,7 @@ namespace MWRender mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); osg::Vec3d cameraPos; getPosition(focal, cameraPos); - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit, collisionType); if (result.mHit) mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 9e2b608dfd..1a5477e89c 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -12,7 +12,7 @@ namespace osg { class Camera; - class NodeCallback; + class Callback; class Node; } @@ -82,7 +82,7 @@ namespace MWRender void updatePosition(); float getCameraDistanceCorrection() const; - osg::ref_ptr mUpdateCallback; + osg::ref_ptr mUpdateCallback; // Used to rotate player to the direction of view after exiting preview or vanity mode. osg::Vec3f mDeferredRotation; diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 44b7b2f457..a9733cddf8 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" @@ -36,7 +37,7 @@ namespace MWRender { - class DrawOnceCallback : public osg::NodeCallback + class DrawOnceCallback : public SceneUtil::NodeCallback { public: DrawOnceCallback () @@ -45,7 +46,7 @@ namespace MWRender { } - void operator () (osg::Node* node, osg::NodeVisitor* nv) override + void operator () (osg::Node* node, osg::NodeVisitor* nv) { if (!mRendered) { @@ -90,7 +91,7 @@ namespace MWRender class SetUpBlendVisitor : public osg::NodeVisitor { public: - SetUpBlendVisitor(): osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mNoAlphaUniform(new osg::Uniform("noAlpha", false)) + SetUpBlendVisitor(): osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } @@ -129,7 +130,7 @@ namespace MWRender } // Disable noBlendAlphaEnv newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); - newStateSet->addUniform(mNoAlphaUniform); + newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON); } if (SceneUtil::getReverseZ() && stateset->getAttribute(osg::StateAttribute::DEPTH)) { @@ -171,8 +172,6 @@ namespace MWRender } traverse(node); } - private: - osg::ref_ptr mNoAlphaUniform; }; CharacterPreview::CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, @@ -215,6 +214,7 @@ namespace MWRender osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); + stateset->setDefine("FORCE_OPAQUE", "1", osg::StateAttribute::ON); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); stateset->setMode(GL_NORMALIZE, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::ON); @@ -252,7 +252,6 @@ namespace MWRender dummyTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); stateset->setTextureAttributeAndModes(7, dummyTexture, osg::StateAttribute::ON); stateset->setTextureAttribute(7, noBlendAlphaEnv, osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("noAlpha", true)); osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.0, 0.0, 0.0, 1.0)); @@ -327,7 +326,6 @@ namespace MWRender void CharacterPreview::setBlendMode() { - mResourceSystem->getSceneManager()->recreateShaders(mNode, "objects", true); SetUpBlendVisitor visitor; mNode->accept(visitor); } @@ -523,7 +521,7 @@ namespace MWRender rebuild(); } - class UpdateCameraCallback : public osg::NodeCallback + class UpdateCameraCallback : public SceneUtil::NodeCallback { public: UpdateCameraCallback(osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) @@ -533,12 +531,10 @@ namespace MWRender { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* cam, osg::NodeVisitor* nv) { - osg::Camera* cam = static_cast(node); - // Update keyframe controllers in the scene graph first... - traverse(node, nv); + traverse(cam, nv); // Now update camera utilizing the updated head position osg::NodePathList nodepaths = mNodeToFollow->getParentalNodePaths(); diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 7d7c6e45df..b248efad4e 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -16,6 +16,7 @@ #include #include +#include #include @@ -62,7 +63,7 @@ namespace } - class CameraUpdateGlobalCallback : public osg::NodeCallback + class CameraUpdateGlobalCallback : public SceneUtil::NodeCallback { public: CameraUpdateGlobalCallback(MWRender::GlobalMap* parent) @@ -71,14 +72,14 @@ namespace { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* node, osg::NodeVisitor* nv) { if (mRendered) { - if (mParent->copyResult(static_cast(node), nv->getTraversalNumber())) + if (mParent->copyResult(node, nv->getTraversalNumber())) { node->setNodeMask(0); - mParent->markForRemoval(static_cast(node)); + mParent->markForRemoval(node); } return; } diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 39022709fb..77c4f0fab5 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -1,11 +1,13 @@ #include "groundcover.hpp" #include +#include #include #include #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" @@ -26,36 +28,6 @@ namespace MWRender } } - void GroundcoverUpdater::setWindSpeed(float windSpeed) - { - mWindSpeed = windSpeed; - } - - void GroundcoverUpdater::setPlayerPos(osg::Vec3f playerPos) - { - mPlayerPos = playerPos; - } - - void GroundcoverUpdater::setDefaults(osg::StateSet *stateset) - { - osg::ref_ptr windUniform = new osg::Uniform("windSpeed", 0.0f); - stateset->addUniform(windUniform.get()); - - osg::ref_ptr playerPosUniform = new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f)); - stateset->addUniform(playerPosUniform.get()); - } - - void GroundcoverUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) - { - osg::ref_ptr windUniform = stateset->getUniform("windSpeed"); - if (windUniform != nullptr) - windUniform->set(mWindSpeed); - - osg::ref_ptr playerPosUniform = stateset->getUniform("playerPos"); - if (playerPosUniform != nullptr) - playerPosUniform->set(mPlayerPos); - } - class InstancingVisitor : public osg::NodeVisitor { public: @@ -66,18 +38,6 @@ namespace MWRender { } - void apply(osg::Node& node) override - { - osg::ref_ptr ss = node.getStateSet(); - if (ss != nullptr) - { - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); - } - - traverse(node); - } - void apply(osg::Geometry& geom) override { for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) @@ -110,32 +70,14 @@ namespace MWRender // Display lists do not support instancing in OSG 3.4 geom.setUseDisplayList(false); + geom.setUseVertexBufferObjects(true); geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); - - osg::ref_ptr ss = geom.getOrCreateStateSet(); - ss->setAttribute(new osg::VertexAttribDivisor(6, 1)); - ss->setAttribute(new osg::VertexAttribDivisor(7, 1)); - - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); - - traverse(geom); } private: std::vector mInstances; osg::Vec3f mChunkPosition; - - void removeAlpha(osg::StateSet* stateset) - { - // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties - stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); - stateset->removeMode(GL_ALPHA_TEST); - stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); - stateset->removeMode(GL_BLEND); - stateset->setRenderBinToInherit(); - } }; class DensityCalculator @@ -199,7 +141,20 @@ namespace MWRender : GenericResourceManager(nullptr) , mSceneManager(sceneManager) , mDensity(density) + , mStateset(new osg::StateSet) { + // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties + // Force a unified alpha handling instead of data from meshes + osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); + mStateset->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + mStateset->setAttributeAndModes(new osg::BlendFunc, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + mStateset->setRenderBinDetails(0, "RenderBin", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1)); + mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1)); + + mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? static_cast(mSceneManager->getShaderManager().getProgramTemplate()->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; + mProgramTemplate->addBindAttribLocation("aOffset", 6); + mProgramTemplate->addBindAttribLocation("aRotation", 7); } void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) @@ -241,7 +196,7 @@ namespace MWRender if (model.empty()) continue; model = "meshes/" + model; - instances[model].emplace_back(std::move(ref), std::move(model)); + instances[model].emplace_back(std::move(ref)); } } } @@ -255,27 +210,23 @@ namespace MWRender for (auto& pair : instances) { const osg::Node* temp = mSceneManager->getTemplate(pair.first); - osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_ALL&(~osg::CopyOp::DEEP_COPY_TEXTURES))); + osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES|osg::CopyOp::DEEP_COPY_USERDATA|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES)); // Keep link to original mesh to keep it in cache group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); - mSceneManager->reinstateRemovedState(node); - InstancingVisitor visitor(pair.second, worldCenter); node->accept(visitor); group->addChild(node); } - // Force a unified alpha handling instead of data from meshes - osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); - group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); - group->getBound(); + group->setStateSet(mStateset); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) group->setCullCallback(new SceneUtil::LightListCallback); - mSceneManager->recreateShaders(group, "groundcover", false, true); - + mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate); + mSceneManager->shareState(group); + group->getBound(); return group; } diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp index 10874b7e8b..ed88f7fe24 100644 --- a/apps/openmw/mwrender/groundcover.hpp +++ b/apps/openmw/mwrender/groundcover.hpp @@ -3,32 +3,11 @@ #include #include -#include #include +#include namespace MWRender { - class GroundcoverUpdater : public SceneUtil::StateSetUpdater - { - public: - GroundcoverUpdater() - : mWindSpeed(0.f) - , mPlayerPos(osg::Vec3f()) - { - } - - void setWindSpeed(float windSpeed); - void setPlayerPos(osg::Vec3f playerPos); - - protected: - void setDefaults(osg::StateSet *stateset) override; - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; - - private: - float mWindSpeed; - osg::Vec3f mPlayerPos; - }; - typedef std::tuple GroundcoverChunkId; // Center, Size class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { @@ -46,16 +25,16 @@ namespace MWRender { ESM::Position mPos; float mScale; - std::string mModel; - GroundcoverEntry(const ESM::CellRef& ref, const std::string& model): - mPos(ref.mPos), mScale(ref.mScale), mModel(model) + GroundcoverEntry(const ESM::CellRef& ref) : mPos(ref.mPos), mScale(ref.mScale) {} }; private: Resource::SceneManager* mSceneManager; float mDensity; + osg::ref_ptr mStateset; + osg::ref_ptr mProgramTemplate; typedef std::map> InstanceMap; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 02f59e0ed0..0560d1485a 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,7 @@ namespace { - class CameraLocalUpdateCallback : public osg::NodeCallback + class CameraLocalUpdateCallback : public SceneUtil::NodeCallback { public: CameraLocalUpdateCallback(MWRender::LocalMap* parent) @@ -44,7 +45,7 @@ namespace { } - void operator()(osg::Node* node, osg::NodeVisitor*) override + void operator()(osg::Camera* node, osg::NodeVisitor*) { if (mRendered) node->setNodeMask(0); @@ -52,12 +53,11 @@ namespace if (!mRendered) { mRendered = true; - mParent->markForRemoval(static_cast(node)); + mParent->markForRemoval(node); } // Note, we intentionally do not traverse children here. The map camera's scene data is the same as the master camera's, // so it has been updated already. - //traverse(node, nv); } private: diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 7fc488020c..7c6d82d83d 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -148,44 +148,6 @@ public: float getValue(osg::NodeVisitor* nv) override; }; -// -------------------------------------------------------------------------------- - -/// Subclass RotateController to add a Z-offset for sneaking in first person mode. -/// @note We use inheritance instead of adding another controller, so that we do not have to compute the worldOrient twice. -/// @note Must be set on a MatrixTransform. -class NeckController : public RotateController -{ -public: - NeckController(osg::Node* relativeTo) - : RotateController(relativeTo) - { - } - - void setOffset(const osg::Vec3f& offset) - { - mOffset = offset; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); - - osg::Quat worldOrient = getWorldOrientation(node); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); - - matrix.setRotate(orient); - matrix.setTrans(matrix.getTrans() + worldOrient.inverse() * mOffset); - - transform->setMatrix(matrix); - - traverse(node,nv); - } - -private: - osg::Vec3f mOffset; -}; - // -------------------------------------------------------------------------------------------------------------- HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) @@ -403,7 +365,7 @@ public: { osgUtil::CullVisitor* cv = static_cast(nv); float fov, aspect, zNear, zFar; - if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar)) + if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar) && std::abs(fov-mFov) > 0.001) { fov = mFov; osg::ref_ptr newProjectionMatrix = new osg::RefMatrix(); @@ -954,7 +916,7 @@ void NpcAnimation::addControllers() if (found != mNodeMap.end()) { osg::MatrixTransform* node = found->second.get(); - mFirstPersonNeckController = new NeckController(mObjectRoot.get()); + mFirstPersonNeckController = new RotateController(mObjectRoot.get()); node->addUpdateCallback(mFirstPersonNeckController); mActiveControllers.emplace_back(node, mFirstPersonNeckController); } @@ -1101,34 +1063,6 @@ Resource::ResourceSystem* NpcAnimation::getResourceSystem() return mResourceSystem; } -void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) -{ - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - if (isNew) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - } - - if (!magicEffect->mHit.empty()) - { - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Don't play particle VFX unless the effect is new or it should be looping. - if (isNew || loop) - addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } -} - void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 768ac01622..b511a52f37 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -24,14 +24,13 @@ namespace MWSound namespace MWRender { -class NeckController; +class RotateController; class HeadAnimationTime; class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: void equipmentChanged() override; - void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; public: typedef std::map PartBoneMap; @@ -97,7 +96,7 @@ private: void setRenderBin(); - osg::ref_ptr mFirstPersonNeckController; + osg::ref_ptr mFirstPersonNeckController; static bool isFirstPersonPart(const ESM::BodyPart* bodypart); static bool isFemalePart(const ESM::BodyPart* bodypart); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 907631436f..88c3d4ba02 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -272,7 +272,7 @@ namespace MWRender : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCurrentStateSet(nullptr) , mCurrentDistance(0.f) - , mAnalyzeMask(analyzeMask) {} + { setTraversalMask(analyzeMask); } typedef std::unordered_map StateSetCounter; struct Result @@ -283,9 +283,6 @@ namespace MWRender void apply(osg::Node& node) override { - if (!(node.getNodeMask() & mAnalyzeMask)) - return; - if (node.getStateSet()) mCurrentStateSet = node.getStateSet(); @@ -308,9 +305,6 @@ namespace MWRender } void apply(osg::Geometry& geom) override { - if (!(geom.getNodeMask() & mAnalyzeMask)) - return; - if (osg::Array* array = geom.getVertexArray()) mResult.mNumVerts += array->getNumElements(); @@ -345,7 +339,6 @@ namespace MWRender osg::StateSet* mCurrentStateSet; StateSetCounter mGlobalStateSetCounter; float mCurrentDistance; - osg::Node::NodeMask mAnalyzeMask; }; class DebugVisitor : public osg::NodeVisitor @@ -390,7 +383,7 @@ namespace MWRender , mRefTrackerLocked(false) { mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); - mDebugBatches = Settings::Manager::getBool("object paging debug batches", "Terrain"); + mDebugBatches = Settings::Manager::getBool("debug chunks", "Terrain"); mMergeFactor = Settings::Manager::getFloat("object paging merge factor", "Terrain"); mMinSize = Settings::Manager::getFloat("object paging min size", "Terrain"); mMinSizeMergeFactor = Settings::Manager::getFloat("object paging min size merge factor", "Terrain"); @@ -658,7 +651,7 @@ namespace MWRender } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS|SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES|SceneUtil::Optimizer::MERGE_GEOMETRY; - mSceneManager->shareState(mergeGroup); + optimizer.optimize(mergeGroup, options); group->addChild(mergeGroup); diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index f846a05872..bc00676d7b 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include "vismask.hpp" @@ -33,7 +34,7 @@ namespace return geom; } - class CullCallback : public osg::NodeCallback + class CullCallback : public SceneUtil::NodeCallback { public: CullCallback() @@ -41,12 +42,11 @@ namespace { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); - unsigned int frame = nv->getTraversalNumber(); + unsigned int frame = cv->getTraversalNumber(); if (frame != mLastFrameNumber) { mLastFrameNumber = frame; @@ -56,7 +56,7 @@ namespace if (!postProcessor) { Log(Debug::Error) << "Failed retrieving user data for master camera: FBO setup failed"; - traverse(node, nv); + traverse(node, cv); return; } @@ -71,7 +71,7 @@ namespace } } - traverse(node, nv); + traverse(node, cv); } private: diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 56d3ba4a43..331ddeca7b 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -77,10 +77,12 @@ namespace MWRender class SharedUniformStateUpdater : public SceneUtil::StateSetUpdater { public: - SharedUniformStateUpdater() + SharedUniformStateUpdater(bool usePlayerUniforms) : mLinearFac(0.f) , mNear(0.f) , mFar(0.f) + , mUsePlayerUniforms(usePlayerUniforms) + , mWindSpeed(0.f) { } @@ -90,6 +92,11 @@ namespace MWRender stateset->addUniform(new osg::Uniform("linearFac", 0.f)); stateset->addUniform(new osg::Uniform("near", 0.f)); stateset->addUniform(new osg::Uniform("far", 0.f)); + if (mUsePlayerUniforms) + { + stateset->addUniform(new osg::Uniform("windSpeed", 0.0f)); + stateset->addUniform(new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f))); + } } void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override @@ -110,6 +117,16 @@ namespace MWRender if (uFar) uFar->set(mFar); + if (mUsePlayerUniforms) + { + auto* windSpeed = stateset->getUniform("windSpeed"); + if (windSpeed) + windSpeed->set(mWindSpeed); + + auto* playerPos = stateset->getUniform("playerPos"); + if (playerPos) + playerPos->set(mPlayerPos); + } } void setProjectionMatrix(const osg::Matrixf& projectionMatrix) @@ -132,11 +149,25 @@ namespace MWRender mFar = far; } + void setWindSpeed(float windSpeed) + { + mWindSpeed = windSpeed; + } + + void setPlayerPos(osg::Vec3f playerPos) + { + mPlayerPos = playerPos; + } + + private: osg::Matrixf mProjectionMatrix; float mLinearFac; float mNear; float mFar; + bool mUsePlayerUniforms; + float mWindSpeed; + osg::Vec3f mPlayerPos; }; class StateUpdater : public SceneUtil::StateSetUpdater @@ -231,7 +262,7 @@ namespace MWRender try { for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) - mResourceSystem->getSceneManager()->cacheInstance(*it); + mResourceSystem->getSceneManager()->getTemplate(*it); for (std::vector::const_iterator it = mTextures.begin(); it != mTextures.end(); ++it) mResourceSystem->getImageManager()->getImage(*it); for (std::vector::const_iterator it = mKeyframes.begin(); it != mKeyframes.end(); ++it) @@ -294,7 +325,6 @@ namespace MWRender // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. osg::ref_ptr sceneRoot = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); - resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); @@ -384,9 +414,10 @@ namespace MWRender const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain"); float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain"); maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f); + bool debugChunks = Settings::Manager::getBool("debug chunks", "Terrain"); mTerrain.reset(new Terrain::QuadTreeWorld( sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, - compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize)); + compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks)); if (Settings::Manager::getBool("object paging", "Terrain")) { mObjectPaging.reset(new ObjectPaging(mResourceSystem->getSceneManager())); @@ -400,29 +431,22 @@ namespace MWRender mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); mTerrain->setWorkQueue(mWorkQueue.get()); - osg::ref_ptr composite = new SceneUtil::CompositeStateSetUpdater; - if (groundcover) { float density = Settings::Manager::getFloat("density", "Groundcover"); density = std::clamp(density, 0.f, 1.f); - mGroundcoverUpdater = new GroundcoverUpdater; - composite->addController(mGroundcoverUpdater); - mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density)); static_cast(mTerrain.get())->addChunkManager(mGroundcover.get()); mResourceSystem->addResourceManager(mGroundcover.get()); - float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); mGroundcover->setViewDistance(groundcoverDistance); } mStateUpdater = new StateUpdater; - composite->addController(mStateUpdater); - sceneRoot->addUpdateCallback(composite); + sceneRoot->addUpdateCallback(mStateUpdater); - mSharedUniformStateUpdater = new SharedUniformStateUpdater; + mSharedUniformStateUpdater = new SharedUniformStateUpdater(groundcover); rootNode->addUpdateCallback(mSharedUniformStateUpdater); mPostProcessor = new PostProcessor(*this, viewer, mRootNode); @@ -462,6 +486,7 @@ namespace MWRender defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); mFog.reset(new FogManager()); @@ -791,15 +816,12 @@ namespace MWRender mSky->update(dt); mWater->update(dt); - if (mGroundcoverUpdater) - { - const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); + osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - float windSpeed = mSky->getBaseWindSpeed(); - mGroundcoverUpdater->setWindSpeed(windSpeed); - mGroundcoverUpdater->setPlayerPos(playerPos); - } + float windSpeed = mSky->getBaseWindSpeed(); + mSharedUniformStateUpdater->setWindSpeed(windSpeed); + mSharedUniformStateUpdater->setPlayerPos(playerPos); } updateNavMesh(); @@ -1259,11 +1281,7 @@ namespace MWRender defines[name] = key; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); - mSceneRoot->removeUpdateCallback(mStateUpdater); - mStateUpdater = new StateUpdater; - mSceneRoot->addUpdateCallback(mStateUpdater); - mStateUpdater->setFogEnd(mViewDistance); - updateAmbient(); + mStateUpdater->reset(); mViewer->startThreading(); } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index b991b08efa..bd8bbe4694 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -70,7 +70,6 @@ namespace DetourNavigator namespace MWRender { - class GroundcoverUpdater; class StateUpdater; class SharedUniformStateUpdater; @@ -279,7 +278,6 @@ namespace MWRender std::unique_ptr mTerrainStorage; std::unique_ptr mObjectPaging; std::unique_ptr mGroundcover; - osg::ref_ptr mGroundcoverUpdater; std::unique_ptr mSky; std::unique_ptr mFog; std::unique_ptr mScreenshotManager; diff --git a/apps/openmw/mwrender/rotatecontroller.cpp b/apps/openmw/mwrender/rotatecontroller.cpp index 534cc74906..d3df4364d0 100644 --- a/apps/openmw/mwrender/rotatecontroller.cpp +++ b/apps/openmw/mwrender/rotatecontroller.cpp @@ -22,21 +22,27 @@ void RotateController::setRotate(const osg::Quat &rotate) mRotate = rotate; } -void RotateController::operator()(osg::Node *node, osg::NodeVisitor *nv) +void RotateController::setOffset(const osg::Vec3f& offset) +{ + mOffset = offset; +} + +void RotateController::operator()(osg::MatrixTransform *node, osg::NodeVisitor *nv) { if (!mEnabled) { traverse(node, nv); return; } - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); + osg::Matrix matrix = node->getMatrix(); osg::Quat worldOrient = getWorldOrientation(node); + osg::Quat worldOrientInverse = worldOrient.inverse(); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); + osg::Quat orient = worldOrient * mRotate * worldOrientInverse * matrix.getRotate(); matrix.setRotate(orient); + matrix.setTrans(matrix.getTrans() + worldOrientInverse * mOffset); - transform->setMatrix(matrix); + node->setMatrix(matrix); traverse(node,nv); } diff --git a/apps/openmw/mwrender/rotatecontroller.hpp b/apps/openmw/mwrender/rotatecontroller.hpp index 9d4080ac6e..1f3ee0f845 100644 --- a/apps/openmw/mwrender/rotatecontroller.hpp +++ b/apps/openmw/mwrender/rotatecontroller.hpp @@ -1,31 +1,36 @@ #ifndef OPENMW_MWRENDER_ROTATECONTROLLER_H #define OPENMW_MWRENDER_ROTATECONTROLLER_H -#include +#include #include +namespace osg +{ + class MatrixTransform; +} + namespace MWRender { /// Applies a rotation in \a relativeTo's space. /// @note Assumes that the node being rotated has its "original" orientation set every frame by a different controller. /// The rotation is then applied on top of that orientation. -/// @note Must be set on a MatrixTransform. -class RotateController : public osg::NodeCallback +class RotateController : public SceneUtil::NodeCallback { public: RotateController(osg::Node* relativeTo); void setEnabled(bool enabled); - + void setOffset(const osg::Vec3f& offset); void setRotate(const osg::Quat& rotate); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); protected: osg::Quat getWorldOrientation(osg::Node* node); bool mEnabled; + osg::Vec3f mOffset; osg::Quat mRotate; osg::Node* mRelativeTo; }; diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index c1b79843a7..dd62d6678b 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include @@ -80,15 +81,15 @@ namespace return mat; } - osg::ref_ptr createTexturedQuad(int numUvSets=1) + osg::ref_ptr createTexturedQuad(int numUvSets=1, float scale=1.f) { osg::ref_ptr geom = new osg::Geometry; osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5, -0.5, 0)); - verts->push_back(osg::Vec3f(-0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, -0.5, 0)); + verts->push_back(osg::Vec3f(-0.5*scale, -0.5*scale, 0)); + verts->push_back(osg::Vec3f(-0.5*scale, 0.5*scale, 0)); + verts->push_back(osg::Vec3f(0.5*scale, 0.5*scale, 0)); + verts->push_back(osg::Vec3f(0.5*scale, -0.5*scale, 0)); geom->setVertexArray(verts); @@ -301,13 +302,11 @@ public: return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); } - class CullCallback : public osg::NodeCallback + class CullCallback : public SceneUtil::NodeCallback { public: - void operator() (osg::Node* node, osg::NodeVisitor* nv) override + void operator() (osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - // XXX have to remove unwanted culling plane of the water reflection camera // Remove all planes that aren't from the standard frustum @@ -336,7 +335,7 @@ public: cv->getProjectionCullingStack().back().pushCurrentMask(); cv->getCurrentCullingSet().pushCurrentMask(); - traverse(node, nv); + traverse(node, cv); cv->getProjectionCullingStack().back().popCurrentMask(); cv->getCurrentCullingSet().popCurrentMask(); @@ -398,7 +397,7 @@ private: /// @note Must be added as cull callback. /// @note Meant to be used on a node that is child of a CameraRelativeTransform. /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public osg::NodeCallback +class UnderwaterSwitchCallback : public SceneUtil::NodeCallback { public: UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) @@ -414,7 +413,7 @@ public: return mEnabled && viewPoint.z() < mWaterLevel; } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (isUnderwater()) return; @@ -622,14 +621,13 @@ private: tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - osg::ref_ptr transform (new osg::PositionAttitudeTransform); + osg::ref_ptr group (new osg::Group); + + mTransform->addChild(group); + const float scale = 2.6f; - transform->setScale(osg::Vec3f(scale,scale,scale)); - - mTransform->addChild(transform); - - osg::ref_ptr geom = createTexturedQuad(); - transform->addChild(geom); + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); osg::StateSet* stateset = geom->getOrCreateStateSet(); @@ -638,7 +636,7 @@ private: stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); - mSunFlashNode = transform; + mSunFlashNode = group; mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); mSunFlashNode->addCullCallback(mSunFlashCallback); @@ -717,7 +715,7 @@ private: } }; - class OcclusionCallback : public osg::NodeCallback + class OcclusionCallback { public: OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) @@ -760,7 +758,7 @@ private: }; /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback + class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback { public: SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) @@ -769,10 +767,8 @@ private: { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); osg::ref_ptr stateset; @@ -788,9 +784,11 @@ private: stateset = new osg::StateSet; stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } - - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } } float scale = visibleRatio; @@ -800,18 +798,20 @@ private: // no traverse return; } + else if (scale == 1.f) + traverse(node, cv); else { osg::Matrix modelView = *cv->getModelViewMatrix(); - modelView.preMultScale(osg::Vec3f(visibleRatio, visibleRatio, visibleRatio)); + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); if (stateset) cv->pushStateSet(stateset); cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); @@ -832,7 +832,7 @@ private: /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback + class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback { public: SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, @@ -854,10 +854,8 @@ private: mColor[i] = std::min(1.f, mColor[i]); } - void operator ()(osg::Node* node, osg::NodeVisitor* nv) override + void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); @@ -885,7 +883,7 @@ private: stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); cv->pushStateSet(stateset); - traverse(node, nv); + traverse(node, cv); cv->popStateSet(); } } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 7a8677e43a..37368b8e7a 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -58,20 +58,17 @@ namespace MWRender /// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); class ClipCullNode : public osg::Group { - class PlaneCullCallback : public osg::NodeCallback + class PlaneCullCallback : public SceneUtil::NodeCallback { public: /// @param cullPlane The culling plane (in world space). PlaneCullCallback(const osg::Plane* cullPlane) - : osg::NodeCallback() - , mCullPlane(cullPlane) + : mCullPlane(cullPlane) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::Polytope::PlaneList origPlaneList = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); osg::Plane plane = *mCullPlane; @@ -83,7 +80,7 @@ class ClipCullNode : public osg::Group cv->getProjectionCullingStack().back().getFrustum().add(plane); - traverse(node, nv); + traverse(node, cv); // undo cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); @@ -93,7 +90,7 @@ class ClipCullNode : public osg::Group const osg::Plane* mCullPlane; }; - class FlipCallback : public osg::NodeCallback + class FlipCallback : public SceneUtil::NodeCallback { public: FlipCallback(const osg::Plane* cullPlane) @@ -101,9 +98,8 @@ class ClipCullNode : public osg::Group { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); osg::Vec3d eyePoint = cv->getEyePoint(); osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); @@ -123,7 +119,7 @@ class ClipCullNode : public osg::Group modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } @@ -166,31 +162,28 @@ private: /// This callback on the Camera has the effect of a RELATIVE_RF_INHERIT_VIEWPOINT transform mode (which does not exist in OSG). /// We want to keep the View Point of the parent camera so we will not have to recreate LODs. -class InheritViewPointCallback : public osg::NodeCallback +class InheritViewPointCallback : public SceneUtil::NodeCallback { public: - InheritViewPointCallback() {} + InheritViewPointCallback() {} - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); osg::ref_ptr modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); cv->popModelViewMatrix(); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT); - traverse(node, nv); + traverse(node, cv); } }; /// Moves water mesh away from the camera slightly if the camera gets too close on the Z axis. /// The offset works around graphics artifacts that occurred with the GL_DEPTH_CLAMP when the camera gets extremely close to the mesh (seen on NVIDIA at least). /// Must be added as a Cull callback. -class FudgeCallback : public osg::NodeCallback +class FudgeCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - const float fudge = 0.2; if (std::abs(cv->getEyeLocal().z()) < fudge) { @@ -203,11 +196,11 @@ public: modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,diff)); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } else - traverse(node, nv); + traverse(node, cv); } }; @@ -670,9 +663,6 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - auto method = mResourceSystem->getSceneManager()->getLightingMethod(); - if (method == SceneUtil::LightingMethod::SingleUBO) - program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); mRainIntensityUpdater = new RainIntensityUpdater(); node->setUpdateCallback(mRainIntensityUpdater); diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 870b8949c7..719e4fdc2b 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include @@ -16,6 +16,7 @@ namespace osg class PositionAttitudeTransform; class Geometry; class Node; + class Callback; } namespace osgUtil @@ -72,7 +73,7 @@ namespace MWRender bool mInterior; osg::Callback* mCullCallback; - osg::ref_ptr mShaderWaterStateSetUpdater; + osg::ref_ptr mShaderWaterStateSetUpdater; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index fb92a4980a..b89cfd8df1 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -173,7 +173,7 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) } void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) + std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index 333dccc915..1f614463a6 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -43,7 +43,7 @@ namespace MWRender /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index d4de8ded5d..c5a4bb6dfc 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -362,7 +362,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - const auto value = static_cast(ptr.getClass().getCreatureStats (ptr).getAiSequence().getLastRunTypeId()); + Interpreter::Type_Integer value = -1; + if(ptr.getClass().isActor()) + { + const auto& stats = ptr.getClass().getCreatureStats(ptr); + if(!stats.isDead() || !stats.isDeathAnimationFinished()) + { + value = static_cast(stats.getAiSequence().getLastRunTypeId()); + } + } runtime.push (value); } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 869a1d540a..3642f68dc2 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -563,13 +563,7 @@ namespace MWScript const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); - effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) - { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - effects += store.getMagicEffects(); - } + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); for (const auto& activeEffect : effects) { @@ -821,7 +815,7 @@ namespace MWScript } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); + runtime.push(stats.getActiveSpells().isSpellActive(id)); } }; @@ -864,6 +858,9 @@ namespace MWScript float param = runtime[0].mFloat; runtime.pop(); + if (param < 0) + throw std::runtime_error("square root of negative number (we aren't that imaginary)"); + runtime.push(std::sqrt (param)); } }; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index fe2df68473..ccad186a0d 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -473,6 +473,8 @@ namespace MWScript ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) { + // Add spell effect to *this actor's* queue immediately + creatureStats.getActiveSpells().addSpell(spell, ptr); // Apply looping particles immediately for constant effects MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } @@ -492,17 +494,6 @@ namespace MWScript runtime.pop(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - // The spell may have an instant effect which must be handled before the spell's removal. - for (const auto& effect : creatureStats.getSpells().getMagicEffects()) - { - if (effect.second.getMagnitude() <= 0) - continue; - MWMechanics::CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) - creatureStats.getSpells().purgeEffect(effect.first.mId); - } - - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); creatureStats.getSpells().remove (id); MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); @@ -527,8 +518,7 @@ namespace MWScript std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); - ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -544,7 +534,7 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index cb71cb56d2..e64e89d775 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -131,7 +131,7 @@ namespace MWSound max = std::max(min, max); Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); - mVfs->normalizeFilename(sfx.mResourceName); + sfx.mResourceName = mVfs->normalizeFilename(sfx.mResourceName); mBufferNameMap.emplace(soundId, &sfx); return &sfx; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index fc9735d576..d2422870d7 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -294,20 +294,9 @@ namespace MWSound if (mMusicFiles.find(playlist) == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); - std::string pattern = "Music/" + playlist; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) - { - if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern) - filelist.push_back(found->first); - else - break; - ++found; - } + for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist)) + filelist.push_back(name); mMusicFiles[playlist] = filelist; } @@ -327,13 +316,11 @@ namespace MWSound if (mMusicFiles.find("Title") == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); // Is there an ini setting for this filename or something? std::string filename = "music/special/morrowind title.mp3"; - auto found = index.find(filename); - if (found != index.end()) + if (mVFS->exists(filename)) { - filelist.emplace_back(found->first); + filelist.emplace_back(filename); mMusicFiles["Title"] = filelist; } else @@ -355,10 +342,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; @@ -389,10 +373,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index fcfafc5f4f..d74dcd82f7 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -6,6 +6,7 @@ #include "../mwmechanics/creaturestats.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "player.hpp" @@ -24,7 +25,7 @@ namespace MWWorld { // Find any NPCs that are following the actor and teleport them with him std::set followers; - getFollowers(actor, followers, true); + getFollowers(actor, followers, mCellName.empty(), true); for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) teleport(*it); @@ -55,14 +56,14 @@ namespace MWWorld int cellY; world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); world->moveObject(actor,world->getExterior(cellX,cellY), - mPosition.asVec3()); + mPosition.asVec3(), true, true); } else - world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3()); + world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3(), true, true); } } - void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles) { + void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles) { std::set followers; MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor, followers); @@ -75,7 +76,7 @@ namespace MWWorld if (!includeHostiles && follower.getClass().getCreatureStats(follower).getAiSequence().isInCombat(actor)) continue; - if (!script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1) + if (!toExterior && !script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1 && follower.getCell()->getCell()->isExterior()) continue; if ((follower.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()).length2() > 800 * 800) diff --git a/apps/openmw/mwworld/actionteleport.hpp b/apps/openmw/mwworld/actionteleport.hpp index 0a981a418e..fcbf59a203 100644 --- a/apps/openmw/mwworld/actionteleport.hpp +++ b/apps/openmw/mwworld/actionteleport.hpp @@ -30,7 +30,7 @@ namespace MWWorld /// @param includeHostiles If true, include hostile followers (which won't actually be teleported) in the output, /// e.g. so that the teleport action can calm them. - static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles = false); + static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles = false); }; } diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index b2dba1d452..590e1e9c00 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -21,6 +21,29 @@ #include "cellstore.hpp" #include "class.hpp" +namespace +{ + template + bool contains(const std::vector& container, + const Contained& contained, float tolerance) + { + for (const auto& pos : contained) + { + bool found = false; + for (const auto& pos2 : container) + { + if ((pos.first-pos2.first).length2() < tolerance*tolerance && pos.second == pos2.second) + { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } +} + namespace MWWorld { @@ -95,7 +118,6 @@ namespace MWWorld { mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); - bool animated = false; size_t slashpos = mesh.find_last_of("/\\"); if (slashpos != std::string::npos && slashpos != mesh.size()-1) { @@ -107,10 +129,7 @@ namespace MWWorld { kfname.replace(kfname.size()-4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) - { mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - animated = true; - } } } } @@ -161,14 +180,6 @@ namespace MWWorld { } - bool storeViews(double referenceTime) - { - for (unsigned int i=0; istoreView(mTerrainViews[i], referenceTime)) - return false; - return true; - } - void doWork() override { for (unsigned int i=0; iisDone()) { - if (!mTerrainPreloadItem->storeViews(timestamp)) - { - if (++mStoreViewsFailCount > 100) - { - OSG_ALWAYS << "paging views are rebuilt every frame, please check for faulty enable/disable scripts." << std::endl; - mStoreViewsFailCount = 0; - } - setTerrainPreloadPositions(std::vector()); - } - else - mStoreViewsFailCount = 0; - mTerrainPreloadItem = nullptr; + mLoadedTerrainPositions = mTerrainPreloadPositions; + mLoadedTerrainTimestamp = timestamp; } } @@ -419,17 +420,7 @@ namespace MWWorld return true; else if (mTerrainPreloadItem->isDone()) { - if (mTerrainPreloadItem->storeViews(timestamp)) - { - mTerrainPreloadItem = nullptr; - return true; - } - else - { - setTerrainPreloadPositions(std::vector()); - setTerrainPreloadPositions(positions); - return false; - } + return true; } else { @@ -440,10 +431,8 @@ namespace MWWorld void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid *exceptPos) { - const float resetThreshold = ESM::Land::REAL_SIZE; - for (const auto& pos : mTerrainPreloadPositions) - if (exceptPos && (pos.first-exceptPos->first).length2() < resetThreshold*resetThreshold && pos.second == exceptPos->second) - return; + if (exceptPos && contains(mTerrainPreloadPositions, std::array {*exceptPos}, ESM::Land::REAL_SIZE)) + return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); @@ -452,29 +441,14 @@ namespace MWWorld setTerrainPreloadPositions(std::vector()); } - bool contains(const std::vector& container, const std::vector& contained) - { - for (const auto& pos : contained) - { - bool found = false; - for (const auto& pos2 : container) - { - if ((pos.first-pos2.first).length2() < 1 && pos.second == pos2.second) - { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } - void CellPreloader::setTerrainPreloadPositions(const std::vector &positions) { if (positions.empty()) + { mTerrainPreloadPositions.clear(); - else if (contains(mTerrainPreloadPositions, positions)) + mLoadedTerrainPositions.clear(); + } + else if (contains(mTerrainPreloadPositions, positions, 128.f)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; @@ -500,5 +474,10 @@ namespace MWWorld } } } + + bool CellPreloader::isTerrainLoaded(const CellPreloader::PositionCellGrid &position, double referenceTime) const + { + return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime && contains(mLoadedTerrainPositions, std::array {position}, ESM::Land::REAL_SIZE); + } } diff --git a/apps/openmw/mwworld/cellpreloader.hpp b/apps/openmw/mwworld/cellpreloader.hpp index e2eea33146..0c3bcb4f99 100644 --- a/apps/openmw/mwworld/cellpreloader.hpp +++ b/apps/openmw/mwworld/cellpreloader.hpp @@ -79,6 +79,7 @@ namespace MWWorld bool syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener); void abortTerrainPreloadExcept(const PositionCellGrid *exceptPos); + bool isTerrainLoaded(const CellPreloader::PositionCellGrid &position, double referenceTime) const; private: Resource::ResourceSystem* mResourceSystem; @@ -93,7 +94,6 @@ namespace MWWorld bool mPreloadInstances; double mLastResourceCacheUpdate; - int mStoreViewsFailCount; struct PreloadEntry { @@ -119,6 +119,9 @@ namespace MWWorld std::vector mTerrainPreloadPositions; osg::ref_ptr mTerrainPreloadItem; osg::ref_ptr mUpdateCacheItem; + + std::vector mLoadedTerrainPositions; + double mLoadedTerrainTimestamp; }; } diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 3d82a30c29..b2ac511509 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -26,6 +26,7 @@ #include "../mwmechanics/recharge.hpp" #include "ptr.hpp" +#include "esmloader.hpp" #include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" @@ -176,6 +177,13 @@ namespace if (state.mVersion < 15) fixRestocking(record, state); + if (state.mVersion < 17) + { + if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); + else if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); + } if (state.mRef.mRefNum.hasContentFile()) { diff --git a/apps/openmw/mwworld/cellvisitors.hpp b/apps/openmw/mwworld/cellvisitors.hpp index 81e1248e6d..77f33fa84b 100644 --- a/apps/openmw/mwworld/cellvisitors.hpp +++ b/apps/openmw/mwworld/cellvisitors.hpp @@ -25,16 +25,6 @@ namespace MWWorld } }; - struct ListObjectsVisitor - { - std::vector mObjects; - - bool operator() (const MWWorld::Ptr& ptr) - { - mObjects.push_back (ptr); - return true; - } - }; } #endif diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 55e39de55e..da4dd9d99e 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -30,12 +30,12 @@ namespace MWWorld } - void Class::insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Class::insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { } - void Class::insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Class::insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const {} bool Class::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 4779a3065b..8e451ea580 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -78,9 +78,9 @@ namespace MWWorld } virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; - virtual void insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const; + virtual void insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; ///< Add reference into a cell for rendering (default implementation: don't render anything). - virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const; + virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; virtual std::string getName (const ConstPtr& ptr) const = 0; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index b12d646e70..a01128fe36 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -2,6 +2,26 @@ #include "esmstore.hpp" #include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/magiceffects.hpp" + +namespace +{ + template + void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName) + { + const T* item = MWBase::Environment::get().getWorld()->getStore().get().search(id); + if(item) + { + enchantment = item->mEnchant; + itemName = item->mName; + } + } +} namespace MWWorld { @@ -28,4 +48,187 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) mStore.load(mEsm[index], &mListener); } + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) + { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + // Convert corprus to format 10 + for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell) + continue; + + ESM::CreatureStats::CorprusStats stats; + stats.mNextWorsening = oldStats.mNextWorsening; + for (int i=0; imEffects.mList) + { + if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + } + creatureStats.mCorprusSpells[id] = stats; + } + // Convert to format 17 + for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = spell->mName; + params.mItem.unset(); + params.mCasterActorId = creatureStats.mActorId; + if(spell->mData.mType == ESM::Spell::ST_Ability) + params.mType = ESM::ActiveSpells::Type_Ability; + else + params.mType = ESM::ActiveSpells::Type_Permanent; + params.mWorsenings = -1; + int effectIndex = 0; + for(const auto& enam : spell->mEffects.mList) + { + if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = effectIndex; + auto rand = oldParams.mEffectRands.find(effectIndex); + if(rand != oldParams.mEffectRands.end()) + { + float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + // Prevent recalculation of resistances + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + } + else + { + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mFlags = ESM::ActiveEffect::Flag_None; + } + params.mEffects.emplace_back(effect); + } + effectIndex++; + } + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + std::multimap equippedItems; + for(std::size_t i = 0; i < inventory.mItems.size(); ++i) + { + const ESM::ObjectState& item = inventory.mItems[i]; + auto slot = inventory.mEquipmentSlots.find(i); + if(slot != inventory.mEquipmentSlots.end()) + equippedItems.emplace(item.mRef.mRefID, slot->second); + } + for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) + { + std::string eId; + std::string name; + switch(store.find(id)) + { + case ESM::REC_ARMO: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_CLOT: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_WEAP: + getEnchantedItem(id, eId, name); + break; + } + if(eId.empty()) + continue; + const ESM::Enchantment* enchantment = store.get().search(eId); + if(!enchantment) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = name; + params.mCasterActorId = creatureStats.mActorId; + params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mWorsenings = -1; + for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + { + const auto& enam = enchantment->mEffects.mList[effectIndex]; + auto [random, multiplier] = oldMagnitudes[effectIndex]; + float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + magnitude *= multiplier; + if(magnitude <= 0) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = static_cast(effectIndex); + // Prevent recalculation of resistances + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + params.mEffects.emplace_back(effect); + } + auto [begin, end] = equippedItems.equal_range(id); + for(auto it = begin; it != end; ++it) + { + params.mItem = { static_cast(it->second), 0 }; + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + } + for(const auto& spell : creatureStats.mCorprusSpells) + { + auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; }); + if(it != creatureStats.mActiveSpells.mSpells.end()) + { + it->mNextWorsening = spell.second.mNextWorsening; + int worsenings = 0; + for(int i = 0; i < ESM::Attribute::Length; ++i) + worsenings = std::max(spell.second.mWorsenings[i], worsenings); + it->mWorsenings = worsenings; + } + } + for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) + { + if(actorId == -1) + continue; + for(auto& params : creatureStats.mActiveSpells.mSpells) + { + if(params.mId == key.mSourceId) + { + bool found = false; + for(auto& effect : params.mEffects) + { + if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) + { + effect.mArg = actorId; + found = true; + break; + } + } + if(found) + break; + } + } + } + // Reset modifiers that were previously recalculated each frame + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + creatureStats.mAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < 3; ++i) + creatureStats.mDynamic[i].mMod = 0.f; + for(std::size_t i = 0; i < 4; ++i) + creatureStats.mAiSettings[i].mMod = 0.f; + if(npcStats) + { + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + npcStats->mSkills[i].mMod = 0.f; + } + } } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index 506105bebb..50631603de 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -13,6 +13,9 @@ namespace ToUTF8 namespace ESM { class ESMReader; + struct CreatureStats; + struct InventoryState; + struct NpcStats; } namespace MWWorld @@ -33,6 +36,8 @@ struct EsmLoader : public ContentLoader ToUTF8::Utf8Encoder* mEncoder; }; +void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr); + } /* namespace MWWorld */ #endif // ESMLOADER_HPP diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 0b8af4463c..49e60af1fd 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -108,11 +108,9 @@ MWWorld::InventoryStore::InventoryStore() MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) : ContainerStore (store) - , mMagicEffects(store.mMagicEffects) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) - , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) , mSelectedEnchantItem(end()) { copySlots (store); @@ -125,9 +123,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor mListener = store.mListener; mInventoryListener = store.mInventoryListener; - mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); @@ -186,8 +182,6 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) @@ -199,7 +193,6 @@ void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) mUpdatesEnabled = true; fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) @@ -554,7 +547,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { mSlots.swap (slots_); fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); flagAsModified(); } } @@ -567,129 +559,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(cons return slots[Slot_CarriedLeft]; } -const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const -{ - return mMagicEffects; -} - -void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) -{ - // To avoid excessive updates during auto-equip - if (!mUpdatesEnabled) - return; - - // Delay update until the listener is set up - if (!mInventoryListener) - return; - - mMagicEffects = MWMechanics::MagicEffects(); - - const auto& stats = actor.getClass().getCreatureStats(actor); - if (stats.isDead() && stats.isDeathAnimationFinished()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector params; - - bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); - if (!existed) - { - params.resize(enchantment.mEffects.mList.size()); - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - int delta = effect.mMagnMax - effect.mMagnMin; - // Roll some dice, one for each effect - if (delta) - params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - // Try resisting each effect - params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); - ++i; - } - - // Note that using the RefID as a key here is not entirely correct. - // Consider equipping the same item twice (e.g. a ring) - // However, permanent enchantments with a random magnitude are kind of an exploit anyway, - // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. - mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; - } - else - params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effect.mEffectID); - - // Fully resisted or can't be applied to target? - if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) - { - i++; - continue; - } - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (!existed) - { - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); - } - - if (magnitude) - mMagicEffects.add (effect, magnitude); - - i++; - } - } - } - - // Now drop expired effects - for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end();) - { - bool found = false; - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter == end()) - continue; - if ((**iter).getCellRef().getRefId() == it->first) - { - found = true; - } - } - if (!found) - mPermanentMagicEffectMagnitudes.erase(it++); - else - ++it; - } - - // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); - - mFirstAutoEquip = false; -} - bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); @@ -800,7 +669,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c if (applyUpdates) { fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } return retval; @@ -848,7 +716,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con return unstack(item, actor, item.getRefData().getCount() - count); } -MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() +MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } @@ -856,7 +724,6 @@ MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) { mInventoryListener = listener; - updateMagicEffects(actor); } void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) @@ -875,105 +742,6 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) */ } -void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) -{ - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - if (enchantmentId.empty()) - continue; - - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) - continue; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - i++; - // Don't get spell icon display information for enchantments that weren't actually applied - if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) - continue; - const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; - magnitude *= params.mMultiplier; - if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); - } - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) -{ - for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) - { - if (*it != end()) - purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) -{ - TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); - if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - if ((*iter)->getCellRef().getRefId() != sourceId) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector& params = effectMagnitudeIt->second; - - int i=0; - for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) - { - if (effectIt->mEffectID != effectId) - continue; - - if (effectIndex >= 0 && effectIndex != i) - continue; - - if (wholeSpell) - { - mPermanentMagicEffectMagnitudes.erase(sourceId); - return; - } - - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (magnitude) - mMagicEffects.add (*effectIt, -magnitude); - - params[i].mMultiplier = 0; - } - } - } -} - void MWWorld::InventoryStore::clear() { mSlots.clear(); @@ -991,38 +759,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) return false; } -void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const +bool MWWorld::InventoryStore::isFirstEquip() { - MWWorld::ContainerStore::writeState(state); - - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector > params; - for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - params.emplace_back(pIt->mRandom, pIt->mMultiplier); - } - - state.mPermanentMagicEffectMagnitudes[it->first] = params; - } -} - -void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) -{ - MWWorld::ContainerStore::readState(state); - - for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); - it != state.mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector params; - for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - EffectParams p; - p.mRandom = pIt->first; - p.mMultiplier = pIt->second; - params.push_back(p); - } - - mPermanentMagicEffectMagnitudes[it->first] = params; - } + bool first = mFirstAutoEquip; + mFirstAutoEquip = false; + return first; } diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 32dc0d2e91..01c53d7028 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -25,15 +25,6 @@ namespace MWWorld */ virtual void equipmentChanged () {} - /** - * @param effect - * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) - * or was it loaded from a savegame / initial game state? \n - * If it isn't new, non-looping VFX should not be played. - * @param playSound Play effect sound? - */ - virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} - virtual ~InventoryStoreListener() = default; }; @@ -68,8 +59,6 @@ namespace MWWorld private: - MWMechanics::MagicEffects mMagicEffects; - InventoryStoreListener* mInventoryListener; // Enables updates of magic effects and actor model whenever items are equipped or unequipped. @@ -78,19 +67,6 @@ namespace MWWorld bool mFirstAutoEquip; - // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. - // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. - struct EffectParams - { - // Modifier to scale between min and max magnitude - float mRandom; - // Multiplier for when an effect was fully or partially resisted - float mMultiplier; - }; - - typedef std::map > TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; - typedef std::vector TSlots; TSlots mSlots; @@ -106,8 +82,6 @@ namespace MWWorld void initSlots (TSlots& slots_); - void updateMagicEffects(const Ptr& actor); - void fireEquipmentChangedEvent(const Ptr& actor); void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; @@ -161,9 +135,6 @@ namespace MWWorld void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. - const MWMechanics::MagicEffects& getMagicEffects() const; - ///< Return magic effects from worn items. - bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; ///< @return true if the two specified objects can stack with each other @@ -198,22 +169,12 @@ namespace MWWorld void setInvListener (InventoryStoreListener* listener, const Ptr& actor); ///< Set a listener for various events, see \a InventoryStoreListener - InventoryStoreListener* getInvListener(); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); - - void purgeEffect (short effectId, bool wholeSpell = false); - ///< Remove a magic effect - - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); - ///< Remove a magic effect + InventoryStoreListener* getInvListener() const; void clear() override; ///< Empty container. - void writeState (ESM::InventoryState& state) const override; - - void readState (const ESM::InventoryState& state) override; + bool isFirstEquip(); }; } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index d8e2fb2f0d..caa0600f7c 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -22,9 +22,10 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" -#include "class.hpp" -#include "ptr.hpp" #include "cellstore.hpp" +#include "class.hpp" +#include "esmloader.hpp" +#include "ptr.hpp" namespace MWWorld { @@ -381,6 +382,14 @@ namespace MWWorld // this is the one object we can not silently drop. throw std::runtime_error ("invalid player state record (object state)"); } + if (reader.getFormat() < 17) + { + convertMagicEffects(player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + player.mSaveAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + player.mSaveSkills[i].mMod = 0.f; + } if (!player.mObject.mEnabled) { diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 76c6ca7548..9ee137fab6 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" @@ -161,7 +162,7 @@ namespace MWWorld } /// Rotates an osg::PositionAttitudeTransform over time. - class RotateCallback : public osg::NodeCallback + class RotateCallback : public SceneUtil::NodeCallback { public: RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2) @@ -170,14 +171,12 @@ namespace MWWorld { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::PositionAttitudeTransform* node, osg::NodeVisitor* nv) { - osg::PositionAttitudeTransform* transform = static_cast(node); - double time = nv->getFrameStamp()->getSimulationTime(); osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis); - transform->setAttitude(orient); + node->setAttitude(orient); traverse(node, nv); } @@ -256,7 +255,7 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection, int slot) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) @@ -278,6 +277,7 @@ namespace MWWorld MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; + state.mSlot = slot; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else @@ -317,7 +317,7 @@ namespace MWWorld // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape if (state.mIdMagic.size() > 1) model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get().find(state.mIdMagic[1])->mModel; - state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true, false); + state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; mMagicBolts.push_back(state); } @@ -342,7 +342,7 @@ namespace MWWorld if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); - state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false, true); + state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false); state.mToDelete = false; mProjectiles.push_back(state); } @@ -493,9 +493,6 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - if (const auto hitWaterPos = projectile->getWaterHitPosition()) - mRendering->emitWaterRipple(Misc::Convert::toOsg(*hitWaterPos)); - const auto pos = projectile->getPosition(); projectileState.mNode->setPosition(pos); @@ -519,6 +516,8 @@ namespace MWWorld if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) bow = *invIt; } + if (projectile->getHitWater()) + mRendering->emitWaterRipple(pos); MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); projectileState.mToDelete = true; @@ -546,10 +545,10 @@ namespace MWWorld cast.mHitPosition = pos; cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; - cast.mStack = false; + cast.mSlot = magicBoltState.mSlot; cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); - MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); magicBoltState.mToDelete = true; } @@ -629,7 +628,7 @@ namespace MWWorld state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; - + state.mSlot = it->mSlot; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; @@ -663,7 +662,7 @@ namespace MWWorld int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false, true); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false); } catch(...) { @@ -685,6 +684,7 @@ namespace MWWorld state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; state.mToDelete = false; + state.mSlot = esm.mSlot; std::string texture; try @@ -716,7 +716,7 @@ namespace MWWorld osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true, false); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index 4dc250dc5f..f889250e1d 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -49,7 +49,7 @@ namespace MWWorld MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); + void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot); void launchProjectile (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile, const osg::Vec3f& pos, const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength); @@ -107,6 +107,7 @@ namespace MWWorld ESM::EffectList mEffects; float mSpeed; + int mSlot; std::vector mSounds; std::set mSoundIds; diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 7398aef77e..e8c5ba35e4 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -55,7 +55,7 @@ namespace MWWorld } RefData::RefData() - : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mCount (1), mCustomData (nullptr), mChanged(false), mFlags(0), mPhysicsPostponed(false) + : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mPhysicsPostponed(false), mCount (1), mCustomData (nullptr), mChanged(false), mFlags(0) { for (int i=0; i<3; ++i) { @@ -65,21 +65,21 @@ namespace MWWorld } RefData::RefData (const ESM::CellRef& cellRef) - : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), + : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mPhysicsPostponed(false), mCount (1), mPosition (cellRef.mPos), mCustomData (nullptr), - mChanged(false), mFlags(0), mPhysicsPostponed(false) // Loading from ESM/ESP files -> assume unchanged + mChanged(false), mFlags(0) // Loading from ESM/ESP files -> assume unchanged { } RefData::RefData (const ESM::ObjectState& objectState, bool deletedByContentFile) : mBaseNode(nullptr), mDeletedByContentFile(deletedByContentFile), - mEnabled (objectState.mEnabled != 0), + mEnabled (objectState.mEnabled != 0), mPhysicsPostponed(false), mCount (objectState.mCount), mPosition (objectState.mPosition), mAnimationState(objectState.mAnimationState), mCustomData (nullptr), - mChanged(true), mFlags(objectState.mFlags), mPhysicsPostponed(false) // Loading from a savegame -> assume changed + mChanged(true), mFlags(objectState.mFlags) // Loading from a savegame -> assume changed { // "Note that the ActivationFlag_UseEnabled is saved to the reference, // which will result in permanently suppressed activation if the reference script is removed. diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index d90224b9bb..c98eb0f2af 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -41,9 +41,12 @@ namespace MWWorld /// separate delete flag used for deletion by a content file /// @note not stored in the save game file. - bool mDeletedByContentFile; + bool mDeletedByContentFile:1; - bool mEnabled; + bool mEnabled:1; + public: + bool mPhysicsPostponed:1; + private: /// 0: deleted int mCount; @@ -63,9 +66,6 @@ namespace MWWorld unsigned int mFlags; public: - - bool mPhysicsPostponed; - RefData(); /// @param cellRef Used to copy constant data such as position into this class where it can diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 399d0e6d7f..ed68a46059 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -104,7 +104,7 @@ namespace } void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics, - MWRender::RenderingManager& rendering, std::set& pagedRefs, bool onlyPhysics) + MWRender::RenderingManager& rendering, std::set& pagedRefs) { if (ptr.getRefData().getBaseNode() || physics.getActor(ptr)) { @@ -114,12 +114,6 @@ namespace std::string model = getModel(ptr, rendering.getResourceSystem()->getVFS()); const auto rotation = makeNodeRotation(ptr, RotationOrder::direct); - if (onlyPhysics && !physics.getObject(ptr) && !ptr.getClass().isActor()) - { - // When we preload physics object we need to skip animated objects. They are dependant on the scene graph which doesn't yet exist. - ptr.getClass().insertObject (ptr, model, rotation, physics, true); - return; - } const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile() || pagedRefs.find(refnum) == pagedRefs.end()) @@ -137,8 +131,7 @@ namespace // Restore effect particles MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); - if (!physics.getObject(ptr)) - ptr.getClass().insertObject (ptr, model, rotation, physics); + ptr.getClass().insertObject (ptr, model, rotation, physics); MWBase::Environment::get().getLuaManager()->objectAddedToScene(ptr); } @@ -200,13 +193,11 @@ namespace struct InsertVisitor { MWWorld::CellStore& mCell; - Loading::Listener& mLoadingListener; - bool mOnlyObjects; - bool mTest; + Loading::Listener* mLoadingListener; std::vector mToInsert; - InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool onlyObjects, bool test); + InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener); bool operator() (const MWWorld::Ptr& ptr); @@ -214,8 +205,8 @@ namespace void insert(AddObject&& addObject); }; - InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool onlyObjects, bool test) - : mCell (cell), mLoadingListener (loadingListener), mOnlyObjects(onlyObjects), mTest(test) + InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener) + : mCell(cell), mLoadingListener(loadingListener) {} bool InsertVisitor::operator() (const MWWorld::Ptr& ptr) @@ -231,7 +222,7 @@ namespace { for (MWWorld::Ptr& ptr : mToInsert) { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled() && (!mOnlyObjects || !ptr.getClass().isActor())) + if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) { try { @@ -244,21 +235,11 @@ namespace } } - if (!mTest) - mLoadingListener.increaseProgress (1); + if (mLoadingListener != nullptr) + mLoadingListener->increaseProgress(1); } } - struct PositionVisitor - { - bool operator() (const MWWorld::Ptr& ptr) - { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) - ptr.getClass().adjustPosition (ptr, false); - return true; - } - }; - int getCellPositionDistanceToOrigin(const std::pair& cellPosition) { return std::abs(cellPosition.first) + std::abs(cellPosition.second); @@ -325,39 +306,12 @@ namespace MWWorld mRendering.update (duration, paused); } - void Scene::unloadInactiveCell (CellStore* cell, bool test) + void Scene::unloadCell(CellStore* cell) { - assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) != mInactiveCells.end()); - if (!test) - Log(Debug::Info) << "Unloading cell " << cell->getCell()->getDescription(); - - ListObjectsVisitor visitor; - - cell->forEach(visitor); - for (const auto& ptr : visitor.mObjects) - { - mPhysics->remove(ptr); - ptr.mRef->mData.mPhysicsPostponed = false; - } - - if (cell->getCell()->isExterior()) - { - const auto cellX = cell->getCell()->getGridX(); - const auto cellY = cell->getCell()->getGridY(); - mPhysics->removeHeightField(cellX, cellY); - } - - mInactiveCells.erase(cell); - } - - void Scene::deactivateCell(CellStore* cell, bool test) - { - assert(mInactiveCells.find(cell) != mInactiveCells.end()); if (mActiveCells.find(cell) == mActiveCells.end()) return; - if (!test) - Log(Debug::Info) << "Deactivate cell " << cell->getCell()->getDescription(); + + Log(Debug::Info) << "Unloading cell " << cell->getCell()->getDescription(); ListAndResetObjectsVisitor visitor; @@ -368,8 +322,8 @@ namespace MWWorld if (const auto object = mPhysics->getObject(ptr)) { mNavigator.removeObject(DetourNavigator::ObjectId(object)); - if (object->isAnimated()) - mPhysics->remove(ptr); + mPhysics->remove(ptr); + ptr.mRef->mData.mPhysicsPostponed = false; } else if (mPhysics->getActor(ptr)) { @@ -387,6 +341,8 @@ namespace MWWorld { if (mPhysics->getHeightField(cellX, cellY) != nullptr) mNavigator.removeHeightfield(osg::Vec2i(cellX, cellY)); + + mPhysics->removeHeightField(cellX, cellY); } if (cell->getCell()->hasWater()) @@ -409,33 +365,41 @@ namespace MWWorld mActiveCells.erase(cell); } - void Scene::activateCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test) + void Scene::loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn) { using DetourNavigator::HeightfieldShape; assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) != mInactiveCells.end()); mActiveCells.insert(cell); - if (test) - Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription(); - else - Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); + Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); const auto world = MWBase::Environment::get().getWorld(); const int cellX = cell->getCell()->getGridX(); const int cellY = cell->getCell()->getGridY(); - if (!test && cell->getCell()->isExterior()) + if (cell->getCell()->isExterior()) { + osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; + const float verts = ESM::Land::LAND_SIZE; + const float worldsize = ESM::Land::REAL_SIZE; + if (data) + { + mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); + } + else + { + static std::vector defaultHeight; + defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); + mPhysics->addHeightField (&defaultHeight[0], cellX, cellY, worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); + } if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) { const osg::Vec2i cellPosition(cellX, cellY); const btVector3& origin = heightField->getCollisionObject()->getWorldTransform().getOrigin(); const osg::Vec3f shift(origin.x(), origin.y(), origin.z()); - const osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); - const ESM::Land::LandData* const data = land == nullptr ? nullptr : land->getData(ESM::Land::DATA_VHGT); const HeightfieldShape shape = [&] () -> HeightfieldShape { if (data == nullptr) @@ -466,103 +430,63 @@ namespace MWWorld if (respawn) cell->respawn(); - insertCell (*cell, loadingListener, false, test); + insertCell(*cell, loadingListener); mRendering.addCell(cell); - if (!test) - { - MWBase::Environment::get().getWindowManager()->addCell(cell); - bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); - float waterLevel = cell->getWaterLevel(); - mRendering.setWaterEnabled(waterEnabled); - if (waterEnabled) - { - mPhysics->enableWater(waterLevel); - mRendering.setWaterHeight(waterLevel); - if (cell->getCell()->isExterior()) + MWBase::Environment::get().getWindowManager()->addCell(cell); + bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); + float waterLevel = cell->getWaterLevel(); + mRendering.setWaterEnabled(waterEnabled); + if (waterEnabled) + { + mPhysics->enableWater(waterLevel); + mRendering.setWaterHeight(waterLevel); + + if (cell->getCell()->isExterior()) + { + if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - { - const btTransform& transform =heightField->getCollisionObject()->getWorldTransform(); - mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, - osg::Vec3f(static_cast(transform.getOrigin().x()), - static_cast(transform.getOrigin().y()), - waterLevel)); - } - } - else - { - mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), - osg::Vec3f(0, 0, waterLevel)); + const btTransform& transform =heightField->getCollisionObject()->getWorldTransform(); + mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, + osg::Vec3f(static_cast(transform.getOrigin().x()), + static_cast(transform.getOrigin().y()), + waterLevel)); } } else - mPhysics->disableWater(); - - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - // The player is loaded before the scene and by default it is grounded, with the scene fully loaded, we validate and correct this. - if (player.mCell == cell) // Only run once, during initial cell load. { - mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f); + mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), + osg::Vec3f(0, 0, waterLevel)); } - - mNavigator.update(player.getRefData().getPosition().asVec3()); - - if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) - mRendering.configureAmbient(cell->getCell()); } + else + mPhysics->disableWater(); + + const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + // The player is loaded before the scene and by default it is grounded, with the scene fully loaded, we validate and correct this. + if (player.mCell == cell) // Only run once, during initial cell load. + { + mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f); + } + + mNavigator.update(player.getRefData().getPosition().asVec3()); + + if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) + mRendering.configureAmbient(cell->getCell()); mPreloader->notifyLoaded(cell); } - void Scene::loadInactiveCell (CellStore *cell, Loading::Listener* loadingListener, bool test) - { - assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) == mInactiveCells.end()); - mInactiveCells.insert(cell); - - if (test) - Log(Debug::Info) << "Testing inactive cell " << cell->getCell()->getDescription(); - else - Log(Debug::Info) << "Loading inactive cell " << cell->getCell()->getDescription(); - - if (!test && cell->getCell()->isExterior()) - { - float verts = ESM::Land::LAND_SIZE; - float worldsize = ESM::Land::REAL_SIZE; - - const int cellX = cell->getCell()->getGridX(); - const int cellY = cell->getCell()->getGridY(); - - osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; - if (data) - { - mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); - } - else - { - static std::vector defaultHeight; - defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); - mPhysics->addHeightField (&defaultHeight[0], cellX, cellY, worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); - } - } - - insertCell (*cell, loadingListener, true, test); - } - void Scene::clear() { - for (auto iter = mInactiveCells.begin(); iter!=mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) { auto* cell = *iter++; - deactivateCell(cell); - unloadInactiveCell (cell); + unloadCell (cell); } assert(mActiveCells.empty()); - assert(mInactiveCells.empty()); mCurrentCell = nullptr; mPreloader->clear(); @@ -604,7 +528,7 @@ namespace MWWorld void Scene::changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent) { - for (auto iter = mInactiveCells.begin(); iter != mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter != mActiveCells.end(); ) { auto* cell = *iter++; if (cell->getCell()->isExterior()) @@ -612,23 +536,20 @@ namespace MWWorld const auto dx = std::abs(playerCellX - cell->getCell()->getGridX()); const auto dy = std::abs(playerCellY - cell->getCell()->getGridY()); if (dx > mHalfGridSize || dy > mHalfGridSize) - deactivateCell(cell); - - if (dx > mHalfGridSize+1 || dy > mHalfGridSize+1) - unloadInactiveCell(cell); + unloadCell(cell); } else - { - deactivateCell(cell); - unloadInactiveCell(cell); - } + unloadCell (cell); } mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); mRendering.setActiveGrid(newGrid); - preloadTerrain(pos, true); + if (mRendering.pagingUnlockCache()) + mPreloader->abortTerrainPreloadExcept(nullptr); + if (!mPreloader->isTerrainLoaded(std::make_pair(pos, newGrid), mRendering.getReferenceTime())) + preloadTerrain(pos, true); mPagedRefs.clear(); mRendering.getPagedRefnums(newGrid, mPagedRefs); @@ -668,7 +589,6 @@ namespace MWWorld } auto cellsPositionsToLoad = cellsToLoad(mActiveCells,mHalfGridSize); - auto cellsPositionsToLoadInactive = cellsToLoad(mInactiveCells,mHalfGridSize+1); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -691,26 +611,12 @@ namespace MWWorld return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); }); - std::sort(cellsPositionsToLoadInactive.begin(), cellsPositionsToLoadInactive.end(), - [&] (const std::pair& lhs, const std::pair& rhs) { - return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); - }); - - // Load cells - for (const auto& [x,y] : cellsPositionsToLoadInactive) - { - if (!isCellInCollection(x, y, mInactiveCells)) - { - CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - loadInactiveCell (cell, loadingListener); - } - } for (const auto& [x,y] : cellsPositionsToLoad) { if (!isCellInCollection(x, y, mActiveCells)) { CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - activateCell (cell, loadingListener, changeEvent); + loadCell (cell, loadingListener, changeEvent); } } @@ -743,17 +649,15 @@ namespace MWWorld loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")..."); CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY); - loadInactiveCell (cell, loadingListener, true); - activateCell (cell, loadingListener, false, true); + loadCell(cell, nullptr, false); - auto iter = mInactiveCells.begin(); - while (iter != mInactiveCells.end()) + auto iter = mActiveCells.begin(); + while (iter != mActiveCells.end()) { if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() && it->mData.mY == (*iter)->getCell()->getGridY()) { - deactivateCell(*iter, true); - unloadInactiveCell (*iter, true); + unloadCell(*iter); break; } @@ -791,18 +695,16 @@ namespace MWWorld loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")..."); CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName); - loadInactiveCell (cell, loadingListener, true); - activateCell (cell, loadingListener, false, true); + loadCell(cell, nullptr, false); - auto iter = mInactiveCells.begin(); - while (iter != mInactiveCells.end()) + auto iter = mActiveCells.begin(); + while (iter != mActiveCells.end()) { assert (!(*iter)->getCell()->isExterior()); if (it->mName == (*iter)->getCell()->mName) { - deactivateCell(*iter, true); - unloadInactiveCell (*iter, true); + unloadCell(*iter); break; } @@ -921,21 +823,18 @@ namespace MWWorld Log(Debug::Info) << "Changing to interior"; // unload - for (auto iter = mInactiveCells.begin(); iter!=mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) { - auto* cell = *iter++; - deactivateCell(cell); - unloadInactiveCell(cell); + auto* cellToUnload = *iter++; + unloadCell(cellToUnload); } assert(mActiveCells.empty()); - assert(mInactiveCells.empty()); loadingListener->setProgressRange(cell->count()); // Load cell. mPagedRefs.clear(); - loadInactiveCell (cell, loadingListener); - activateCell (cell, loadingListener, changeEvent); + loadCell(cell, loadingListener, changeEvent); changePlayerCell(cell, position, adjustPlayerPos); @@ -985,26 +884,19 @@ namespace MWWorld mCellChanged = false; } - void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool onlyObjects, bool test) + void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener) { - InsertVisitor insertVisitor (cell, *loadingListener, onlyObjects, test); + InsertVisitor insertVisitor(cell, loadingListener); cell.forEach (insertVisitor); - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs, onlyObjects); }); - if (!onlyObjects) - { - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); - - // do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order - PositionVisitor posVisitor; - cell.forEach (posVisitor); - } + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs); }); + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); } void Scene::addObjectToScene (const Ptr& ptr) { try { - addObject(ptr, *mPhysics, mRendering, mPagedRefs, false); + addObject(ptr, *mPhysics, mRendering, mPagedRefs); addObject(ptr, *mPhysics, mNavigator); MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); @@ -1016,9 +908,9 @@ namespace MWWorld } } - void Scene::removeObjectFromScene (const Ptr& ptr) + void Scene::removeObjectFromScene (const Ptr& ptr, bool keepActive) { - MWBase::Environment::get().getMechanicsManager()->remove (ptr); + MWBase::Environment::get().getMechanicsManager()->remove (ptr, keepActive); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); if (const auto object = mPhysics->getObject(ptr)) @@ -1241,10 +1133,7 @@ namespace MWWorld { std::vector vec; vec.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); - if (sync && mRendering.pagingUnlockCache()) - mPreloader->abortTerrainPreloadExcept(nullptr); - else - mPreloader->abortTerrainPreloadExcept(&vec[0]); + mPreloader->abortTerrainPreloadExcept(&vec[0]); mPreloader->setTerrainPreloadPositions(vec); if (!sync) return; diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 75c070dd1e..623bddbc82 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -77,7 +77,6 @@ namespace MWWorld CellStore* mCurrentCell; // the cell the player is in CellStoreCollection mActiveCells; - CellStoreCollection mInactiveCells; bool mCellChanged; MWPhysics::PhysicsSystem *mPhysics; MWRender::RenderingManager& mRendering; @@ -100,7 +99,7 @@ namespace MWWorld std::vector> mWorkItems; - void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool onlyObjects, bool test = false); + void insertCell(CellStore &cell, Loading::Listener* loadingListener); osg::Vec2i mCurrentGridCenter; // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center @@ -116,10 +115,8 @@ namespace MWWorld osg::Vec4i gridCenterToBounds(const osg::Vec2i ¢erCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f &pos, const osg::Vec2i *currentGridCenter = nullptr) const; - void unloadInactiveCell (CellStore* cell, bool test = false); - void deactivateCell (CellStore* cell, bool test = false); - void activateCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false); - void loadInactiveCell (CellStore *cell, Loading::Listener* loadingListener, bool test = false); + void unloadCell(CellStore* cell); + void loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn); public: @@ -161,7 +158,7 @@ namespace MWWorld void addObjectToScene (const Ptr& ptr); ///< Add an object that already exists in the world model to the scene. - void removeObjectFromScene (const Ptr& ptr); + void removeObjectFromScene (const Ptr& ptr, bool keepActive = false); ///< Remove an object from the scene, but not from the world model. void removeFromPagedRefs(const Ptr &ptr); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index bc28c2eb7b..299cc8676b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -76,21 +77,6 @@ #include "contentloader.hpp" #include "esmloader.hpp" -namespace -{ - -// Wraps a value to (-PI, PI] -void wrap(float& rad) -{ - const float pi = static_cast(osg::PI); - if (rad>0) - rad = std::fmod(rad+pi, 2.0f*pi)-pi; - else - rad = std::fmod(rad-pi, 2.0f*pi)+pi; -} - -} - namespace MWWorld { struct GameContentLoader : public ContentLoader @@ -1128,7 +1114,7 @@ namespace MWWorld } } - MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics) + MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics, bool keepActive) { ESM::Position pos = ptr.getRefData().getPosition(); std::memcpy(pos.pos, &position, sizeof(osg::Vec3f)); @@ -1173,7 +1159,8 @@ namespace MWWorld if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); - mWorldScene->addObjectToScene(newPtr); + if(newPtr.getRefData().isEnabled()) + mWorldScene->addObjectToScene(newPtr); std::string script = newPtr.getClass().getScript(newPtr); if (!script.empty()) @@ -1184,7 +1171,7 @@ namespace MWWorld } else if (!newCellActive && currCellActive) { - mWorldScene->removeObjectFromScene(ptr); + mWorldScene->removeObjectFromScene(ptr, keepActive); mLocalScripts.remove(ptr); removeContainerScripts (ptr); haveToMove = false; @@ -1290,8 +1277,6 @@ namespace MWWorld void World::rotateObject(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) { - const float pi = static_cast(osg::PI); - ESM::Position pos = ptr.getRefData().getPosition(); float *objRot = pos.rot; if (flags & MWBase::RotationFlag_adjust) @@ -1313,13 +1298,9 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - const float half_pi = pi/2.f; - - if(objRot[0] < -half_pi) objRot[0] = -half_pi; - else if(objRot[0] > half_pi) objRot[0] = half_pi; - - wrap(objRot[1]); - wrap(objRot[2]); + objRot[0] = osg::clampBetween(objRot[0], -osg::PIf / 2, osg::PIf / 2); + objRot[1] = Misc::normalizeAngle(objRot[1]); + objRot[2] = Misc::normalizeAngle(objRot[2]); } ptr.getRefData().setPosition(pos); @@ -1881,7 +1862,7 @@ namespace MWWorld mRendering->getCamera()->setSneakOffset(0.f); int blind = 0; - auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); + const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); @@ -2452,7 +2433,7 @@ namespace MWWorld else { // Remove the old CharacterController - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); @@ -2468,7 +2449,7 @@ namespace MWWorld void World::renderPlayer() { - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); MWWorld::Ptr player = getPlayerPtr(); @@ -3128,7 +3109,20 @@ namespace MWWorld { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (inv.getSelectedEnchantItem() != inv.end()) - cast.cast(*inv.getSelectedEnchantItem()); + { + const auto& itemPtr = *inv.getSelectedEnchantItem(); + auto [slots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + int slot = 0; + for(std::size_t i = 0; i < slots.size(); ++i) + { + if(inv.getSlot(slots[i]) == inv.getSelectedEnchantItem()) + { + slot = slots[i]; + break; + } + } + cast.cast(itemPtr, slot); + } } } @@ -3145,6 +3139,7 @@ namespace MWWorld bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); if (underwater) { + MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); mRendering->emitWaterRipple(worldPos); return; } @@ -3162,9 +3157,9 @@ namespace MWWorld mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } - void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) + void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) { - mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); + mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection, slot); } void World::updateProjectilesCasters() @@ -3172,46 +3167,24 @@ namespace MWWorld mProjectileManager->updateCasters(); } - class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - - public: - ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) - : mActor(actor) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override - { - const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const auto magicEffect = store.get().find(key.mId); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) - return; - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = store.get().find (magicEffect->mHit); - else - castStatic = store.get().find ("VFX_DefaultHit"); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); - } - }; - void World::applyLoopingParticles(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { - ApplyLoopingParticlesVisitor visitor(ptr); - cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); - cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); - if (cls.hasInventoryStore(ptr)) - cls.getInventoryStore(ptr).visitEffectSources(visitor); + std::set playing; + for(const auto& params : cls.getCreatureStats(ptr).getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(playing.insert(effect.mEffectId).second) + { + const auto magicEffect = getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) + MWMechanics::playEffects(ptr, *magicEffect, false); + } + } + } } } @@ -3222,10 +3195,7 @@ namespace MWWorld void World::breakInvisibility(const Ptr &actor) { - actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility); - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(actor, ESM::MagicEffect::Invisibility); // Normally updated once per frame, but here it is kinda important to do it right away. MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); @@ -3718,7 +3688,7 @@ namespace MWWorld } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, - const std::string& id, const std::string& sourceName, const bool fromProjectile) + const std::string& id, const std::string& sourceName, const bool fromProjectile, int slot) { std::map > toApply; for (const ESM::ENAMstruct& effectInfo : effects.mList) @@ -3796,7 +3766,7 @@ namespace MWWorld cast.mHitPosition = origin; cast.mId = id; cast.mSourceName = sourceName; - cast.mStack = false; + cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); @@ -3953,9 +3923,10 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); + return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 32fc9b101a..6e48f045c0 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -373,7 +373,7 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) override; ///< @return an updated Ptr in case the Ptr's cell changes - MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true) override; + MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; @@ -636,7 +636,7 @@ namespace MWWorld */ void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; - void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; + void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) override; void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override; void updateProjectilesCasters() override; @@ -681,7 +681,7 @@ namespace MWWorld void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, - const bool fromProjectile=false) override; + const bool fromProjectile=false, int slot = 0) override; void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; @@ -735,7 +735,8 @@ namespace MWWorld bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp new file mode 100644 index 0000000000..167c275598 --- /dev/null +++ b/apps/openmw/options.cpp @@ -0,0 +1,116 @@ +#include "options.hpp" + +#include +#include +#include + +#include + +namespace +{ + namespace bpo = boost::program_options; + typedef std::vector StringsVector; +} + +namespace OpenMW +{ + bpo::options_description makeOptionsDescription() + { + bpo::options_description desc("Syntax: openmw \nAllowed options"); + + desc.add_options() + ("help", "print help message") + ("version", "print version information and quit") + + ("replace", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended") + + ("data", bpo::value()->default_value(Files::PathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::PathContainer::value_type(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(boost::filesystem::path(), "resources"), + "set resources directory") + + ("start", bpo::value()->default_value(""), + "set initial cell") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + + ("groundcover", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") + + ("lua-scripts", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") + + ("no-sound", bpo::value()->implicit_value(true) + ->default_value(false), "disable all sounds") + + ("script-all", bpo::value()->implicit_value(true) + ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") + + ("script-all-dialogue", bpo::value()->implicit_value(true) + ->default_value(false), "compile all dialogue scripts at startup") + + ("script-console", bpo::value()->implicit_value(true) + ->default_value(false), "enable console-only script functionality") + + ("script-run", bpo::value()->default_value(""), + "select a file containing a list of console commands that is executed on startup") + + ("script-warn", bpo::value()->implicit_value (1) + ->default_value (1), + "handling of warnings when compiling scripts\n" + "\t0 - ignore warning\n" + "\t1 - show warning but consider script as correctly compiled anyway\n" + "\t2 - treat warnings as errors") + + ("script-blacklist", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") + + ("script-blacklist-use", bpo::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting") + + ("load-savegame", bpo::value()->default_value(boost::filesystem::path(), ""), + "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") + + ("skip-menu", bpo::value()->implicit_value(true) + ->default_value(false), "skip main menu on game startup") + + ("new-game", bpo::value()->implicit_value(true) + ->default_value(false), "run new game sequence (ignored if skip-menu=0)") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + + ("fallback", bpo::value()->default_value(Fallback::FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + + ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") + + ("export-fonts", bpo::value()->implicit_value(true) + ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") + + ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") + + ("random-seed", bpo::value () + ->default_value(Misc::Rng::generateDefaultSeed()), + "seed value for random number generator") + ; + + return desc; + } +} diff --git a/apps/openmw/options.hpp b/apps/openmw/options.hpp new file mode 100644 index 0000000000..246999eb19 --- /dev/null +++ b/apps/openmw/options.hpp @@ -0,0 +1,11 @@ +#ifndef APPS_OPENMW_OPTIONS_H +#define APPS_OPENMW_OPTIONS_H + +#include + +namespace OpenMW +{ + boost::program_options::options_description makeOptionsDescription(); +} + +#endif diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 7800efac3d..5665f59508 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -40,6 +40,9 @@ if (GTEST_FOUND AND GMOCK_FOUND) shader/parsedefines.cpp shader/parsefors.cpp shader/shadermanager.cpp + + ../openmw/options.cpp + openmw/options.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 9405dba4b2..32d4ea49b8 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -119,7 +119,7 @@ return { EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); - EXPECT_EQ(mLua.getMutableFromReadOnly(mLua.makeReadOnly(script)), script); + EXPECT_EQ(LuaUtil::getMutableFromReadOnly(LuaUtil::makeReadOnly(script)), script); } TEST_F(LuaStateTest, Print) @@ -150,8 +150,8 @@ return { { LuaUtil::LuaState lua(mVFS.get()); - sol::table api1 = lua.makeReadOnly(lua.sol().create_table_with("name", "api1")); - sol::table api2 = lua.makeReadOnly(lua.sol().create_table_with("name", "api2")); + sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); + sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api1}}); diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp index fb8e48e461..934cbf761a 100644 --- a/apps/openmw_test_suite/lua/test_utilpackage.cpp +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -10,30 +10,41 @@ namespace { using namespace testing; + template + T get(sol::state& lua, std::string luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + std::string getAsString(sol::state& lua, std::string luaCode) + { + return LuaUtil::toString(lua.safe_script("return " + luaCode)); + } + TEST(LuaUtilPackageTest, Vector2) { sol::state lua; lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); lua["util"] = LuaUtil::initUtilPackage(lua); lua.safe_script("v = util.vector2(3, 4)"); - EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), 3); - EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 4); - EXPECT_EQ(lua.safe_script("return tostring(v)").get(), "(3, 4)"); - EXPECT_FLOAT_EQ(lua.safe_script("return v:length()").get(), 5); - EXPECT_FLOAT_EQ(lua.safe_script("return v:length2()").get(), 25); - EXPECT_FALSE(lua.safe_script("return util.vector2(1, 2) == util.vector2(1, 3)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) == util.vector2(2, 4) / 2").get()); - EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) * 2 == util.vector2(2, 4)").get()); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2) * v").get(), 17); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2):dot(v)").get(), 17); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 3); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 4); + EXPECT_EQ(get(lua, "tostring(v)"), "(3, 4)"); + EXPECT_FLOAT_EQ(get(lua, "v:length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "v:length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector2(1, 2) == util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) == util.vector2(2, 4) / 2")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) * 2 == util.vector2(2, 4)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2) * v"), 17); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2):dot(v)"), 17); EXPECT_ERROR(lua.safe_script("v2, len = v.normalize()"), "value is not a valid userdata"); // checks that it doesn't segfault lua.safe_script("v2, len = v:normalize()"); - EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 5); - EXPECT_TRUE(lua.safe_script("return v2 == util.vector2(3/5, 4/5)").get()); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector2(3/5, 4/5)")); lua.safe_script("_, len = util.vector2(0, 0):normalize()"); - EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 0); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); } TEST(LuaUtilPackageTest, Vector3) @@ -42,27 +53,59 @@ namespace lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); lua["util"] = LuaUtil::initUtilPackage(lua); lua.safe_script("v = util.vector3(5, 12, 13)"); - EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), 5); - EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 12); - EXPECT_FLOAT_EQ(lua.safe_script("return v.z").get(), 13); - EXPECT_EQ(lua.safe_script("return tostring(v)").get(), "(5, 12, 13)"); - EXPECT_EQ(LuaUtil::toString(lua.safe_script("return v")), "(5, 12, 13)"); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length()").get(), 5); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length2()").get(), 25); - EXPECT_FALSE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(1, 3, 2)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)").get()); - EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2").get()); - EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)").get()); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1) * v").get(), 5*3 + 12*2 + 13*1); - EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1):dot(v)").get(), 5*3 + 12*2 + 13*1); - EXPECT_TRUE(lua.safe_script("return util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)").get()); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 5); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); + EXPECT_FLOAT_EQ(get(lua, "v.z"), 13); + EXPECT_EQ(get(lua, "tostring(v)"), "(5, 12, 13)"); + EXPECT_EQ(getAsString(lua, "v"), "(5, 12, 13)"); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector3(1, 2, 3) == util.vector3(1, 3, 2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1) * v"), 5*3 + 12*2 + 13*1); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1):dot(v)"), 5*3 + 12*2 + 13*1); + EXPECT_TRUE(get(lua, "util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)")); EXPECT_ERROR(lua.safe_script("v2, len = util.vector3(3, 4, 0).normalize()"), "value is not a valid userdata"); lua.safe_script("v2, len = util.vector3(3, 4, 0):normalize()"); - EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 5); - EXPECT_TRUE(lua.safe_script("return v2 == util.vector3(3/5, 4/5, 0)").get()); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector3(3/5, 4/5, 0)")); lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()"); - EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 0); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + } + + TEST(LuaUtilPackageTest, Transform) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua["T"] = lua["util"]["transform"]; + lua["v"] = lua["util"]["vector3"]; + EXPECT_ERROR(lua.safe_script("T.identity = nil"), "attempt to index"); + EXPECT_EQ(getAsString(lua, "T.identity * v(3, 4, 5)"), "(3, 4, 5)"); + EXPECT_EQ(getAsString(lua, "T.move(1, 2, 3) * v(3, 4, 5)"), "(4, 6, 8)"); + EXPECT_EQ(getAsString(lua, "T.scale(1, -2, 3) * v(3, 4, 5)"), "(3, -8, 15)"); + EXPECT_EQ(getAsString(lua, "T.scale(v(1, 2, 3)) * v(3, 4, 5)"), "(3, 8, 15)"); + lua.safe_script("moveAndScale = T.move(v(1, 2, 3)) * T.scale(0.5, 1, 0.5) * T.move(10, 20, 30)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(0, 0, 0)"), "(6, 22, 18)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)"); + EXPECT_EQ(getAsString(lua, "moveAndScale"), "TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) }"); + EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }"); + lua.safe_script("rx = T.rotateX(math.pi / 2)"); + lua.safe_script("ry = T.rotateY(math.pi / 2)"); + lua.safe_script("rz = T.rotateZ(math.pi / 2)"); + EXPECT_LT(get(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6); + lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(-math.pi / 4)"); + EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ")); + EXPECT_LT(get(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6); + lua.safe_script("rz_move_rx = rz * T.move(0, 3, 0) * rx"); + EXPECT_LT(get(lua, "(rz_move_rx * v(1, 2, 3) - v(0, 1, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz_move_rx:inverse() * v(0, 1, 2) - v(1, 2, 3)):length()"), 1e-6); } TEST(LuaUtilPackageTest, UtilityFunctions) @@ -71,12 +114,12 @@ namespace lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); lua["util"] = LuaUtil::initUtilPackage(lua); lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))"); - EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), -0.5); - EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 0.86602539); - EXPECT_FLOAT_EQ(lua.safe_script("return util.normalizeAngle(math.pi * 10 + 0.1)").get(), 0.1); - EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(0.1, 0, 1.5)").get(), 0.1); - EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(-0.1, 0, 1.5)").get(), 0); - EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(2.1, 0, 1.5)").get(), 1.5); + EXPECT_FLOAT_EQ(get(lua, "v.x"), -0.5); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 0.86602539); + EXPECT_FLOAT_EQ(get(lua, "util.normalizeAngle(math.pi * 10 + 0.1)"), 0.1); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(0.1, 0, 1.5)"), 0.1); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(-0.1, 0, 1.5)"), 0); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(2.1, 0, 1.5)"), 1.5); } } diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp index 431725be2c..6128c7e5dd 100644 --- a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp +++ b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp @@ -27,9 +27,9 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution) search.highlightKeywords(text.begin(), text.end(), matches); // Should contain: "foo bar", "lock switch" - ASSERT_TRUE (matches.size() == 2); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "foo bar"); - ASSERT_TRUE (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) == "lock switch"); + EXPECT_EQ (matches.size() , 2); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "foo bar"); + EXPECT_EQ (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) , "lock switch"); } TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) @@ -43,8 +43,8 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "dwemer language"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "dwemer language"); } @@ -62,6 +62,75 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "bar lock"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "bar lock"); } + + +TEST_F(KeywordSearchTest, keyword_test_utf8_word_begin) +{ + // We make sure that the search works well even if the character is not ASCII + MWDialogue::KeywordSearch search; + search.seed("états", 0); + search.seed("ïrradiés", 0); + search.seed("ça nous déçois", 0); + search.seed("ois", 0); + + std::string text = "les nations unis ont réunis le monde entier, états units inclus pour parler du problème des gens ïrradiés et ça nous déçois"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ (matches.size() , 3); + EXPECT_EQ (std::string( matches[0].mBeg, matches[0].mEnd) , "états"); + EXPECT_EQ (std::string( matches[1].mBeg, matches[1].mEnd) , "ïrradiés"); + EXPECT_EQ (std::string( matches[2].mBeg, matches[2].mEnd) , "ça nous déçois"); +} + +TEST_F(KeywordSearchTest, keyword_test_non_alpha_non_whitespace_word_begin) +{ + // We make sure that the search works well even if the separator is not a whitespace + MWDialogue::KeywordSearch search; + search.seed("Report to caius cosades", 0); + + + + std::string text = "I was told to \"Report to caius cosades\""; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Report to caius cosades"); +} + +TEST_F(KeywordSearchTest, keyword_test_russian_non_ascii_before) +{ + // We make sure that the search works well even if the separator is not a whitespace with russian chars + MWDialogue::KeywordSearch search; + search.seed("Доложить Каю Косадесу", 0); + + std::string text = "Что? Да. Я Кай Косадес. То есть как это, вам велели «Доложить Каю Косадесу»? О чем вы говорите?"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); +} + +TEST_F(KeywordSearchTest, keyword_test_russian_ascii_before) +{ + // We make sure that the search works well even if the separator is not a whitespace with russian chars + MWDialogue::KeywordSearch search; + search.seed("Доложить Каю Косадесу", 0); + + std::string text = "Что? Да. Я Кай Косадес. То есть как это, вам велели 'Доложить Каю Косадесу'? О чем вы говорите?"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); +} + diff --git a/apps/openmw_test_suite/openmw/options.cpp b/apps/openmw_test_suite/openmw/options.cpp new file mode 100644 index 0000000000..9de7613d19 --- /dev/null +++ b/apps/openmw_test_suite/openmw/options.cpp @@ -0,0 +1,379 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace OpenMW; + + namespace bpo = boost::program_options; + + std::vector generateSupportedCharacters(std::vector&& base = {}) + { + std::vector result = std::move(base); + for (int i = 1; i <= std::numeric_limits::max(); ++i) + if (i != '&' && i != '"' && i != ' ' && i != '@' && i != '\n') + result.push_back(std::string(1, i)); + return result; + } + + constexpr std::array supportedAtSignEscapings { + std::pair {'a', '@'}, + std::pair {'h', '#'}, + }; + + MATCHER_P(IsPath, v, "") { return arg.string() == v; } + + template + void parseArgs(const T& arguments, bpo::variables_map& variables, bpo::options_description& description) + { + Files::parseArgs(static_cast(arguments.size()), arguments.data(), variables, description); + } + + TEST(OpenMWOptionsFromArguments, should_support_equality_to_separate_flag_and_value) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame=save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_single_word_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "/home/user/openmw/save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "/home/user/openmw/save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_windows_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(C:\OpenMW\save.omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(C:\OpenMW\save.omwsave)"); + } + + /*TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_spaces) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my"); +// EXPECT_EQ(variables["load-savegame"].as().string(), "my save.omwsave"); + }*/ + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_octothorpe) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my#save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my#save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_at_sign) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my@save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); +// EXPECT_EQ(variables["load-savegame"].as().string(), "my?ave.omwsave"); + EXPECT_EQ(variables["load-savegame"].as().string(), "my@save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_quote) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(my"save.omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(my"save.omwsave)"); + } + +/* TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save".omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save)"); +// EXPECT_EQ(variables["load-savegame"].as().string(), R"("save".omwsave)"); + }*/ + + TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save&".omwsave")"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save.omwsave&&")"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave&"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "save&.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save&.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_multiple_quotes) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(my"save".omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(my"save".omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_compose_data) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--data", "1", "--data", "2"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromArguments, should_compose_data_from_single_flag) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--data", "1", "2"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromArguments, should_throw_on_multiple_load_savegame) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "1.omwsave", "--load-savegame", "2.omwsave"}; + bpo::variables_map variables; + EXPECT_THROW(parseArgs(arguments, variables, description), std::exception); + } + + struct OpenMWOptionsFromArgumentsStrings : TestWithParam {}; + + TEST_P(OpenMWOptionsFromArgumentsStrings, should_support_paths_with_certain_characters_in_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::string path = "save_" + std::string(GetParam()) + ".omwsave"; + const std::string pathArgument = "\"" + path + "\""; + const std::array arguments {"openmw", "--load-savegame", pathArgument.c_str()}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), path); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedCharacters, + OpenMWOptionsFromArgumentsStrings, + ValuesIn(generateSupportedCharacters({u8"👍", u8"Ъ", u8"Ǽ", "\n"})) + ); + + struct OpenMWOptionsFromArgumentsEscapings : TestWithParam> {}; + +/* TEST_P(OpenMWOptionsFromArgumentsEscapings, should_support_escaping_with_at_sign_in_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::string path = "save_@" + std::string(1, GetParam().first) + ".omwsave"; + const std::array arguments {"openmw", "--load-savegame", path.c_str()}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), + "save_" + std::string(1, GetParam().second) + ".omwsave"); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedEscapingsWithAtSign, + OpenMWOptionsFromArgumentsEscapings, + ValuesIn(supportedAtSignEscapings) + );*/ + + TEST(OpenMWOptionsFromConfig, should_support_single_word_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save.omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + +/* TEST(OpenMWOptionsFromConfig, should_strip_outer_quotes_from_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=""save".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), ""); +// EXPECT_EQ(variables["load-savegame"].as().string(), R"(""save".omwsave")"); + }*/ + + TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path_with_space) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="my save.omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_octothorpe) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save#.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save#.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_at_sign) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save@.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save@.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_quote) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=save".omwsave)"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_ignore_commented_option) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("#load-savegame=save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), ""); + } + + TEST(OpenMWOptionsFromConfig, should_throw_on_multiple_load_savegame) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=1.omwsave\nload-savegame=2.omwsave"); + bpo::variables_map variables; + EXPECT_THROW(Files::parseConfig(stream, variables, description), std::exception); + } + + TEST(OpenMWOptionsFromConfig, should_support_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=/home/user/openmw/save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "/home/user/openmw/save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_windows_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=C:\OpenMW\save.omwsave)"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(C:\OpenMW\save.omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_compose_data) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("data=1\ndata=2"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save&".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save.omwsave&&")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave&"); + } + + TEST(OpenMWOptionsFromConfig, should_support_load_savegame_path_with_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save&.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save&.omwsave"); + } + + struct OpenMWOptionsFromConfigStrings : TestWithParam {}; + + TEST_P(OpenMWOptionsFromConfigStrings, should_support_paths_with_certain_characters_in_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::string path = "save_" + std::string(GetParam()) + ".omwsave"; + std::istringstream stream("load-savegame=\"" + path + "\""); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), path); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedCharacters, + OpenMWOptionsFromConfigStrings, + ValuesIn(generateSupportedCharacters({u8"👍", u8"Ъ", u8"Ǽ"})) + ); +} diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 2558ec81de..03cd63480a 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,4 +1,3 @@ - set(WIZARD componentselectionpage.cpp conclusionpage.cpp @@ -34,21 +33,6 @@ set(WIZARD_HEADER utils/componentlistwidget.hpp ) -# Headers that must be pre-processed -set(WIZARD_HEADER_MOC - componentselectionpage.hpp - conclusionpage.hpp - existinginstallationpage.hpp - importpage.hpp - installationtargetpage.hpp - intropage.hpp - languageselectionpage.hpp - mainwizard.hpp - methodselectionpage.hpp - - utils/componentlistwidget.hpp -) - set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui @@ -63,7 +47,6 @@ set(WIZARD_UI if (OPENMW_USE_UNSHIELD) set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) - set (WIZARD_HEADER_MOC ${WIZARD_HEADER_MOC} installationpage.hpp unshield/unshieldworker.hpp) set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) add_definitions(-DOPENMW_USE_UNSHIELD) endif (OPENMW_USE_UNSHIELD) @@ -80,7 +63,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) -QT5_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${WIZARD_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -94,7 +76,6 @@ openmw_add_executable(openmw-wizard ${WIZARD} ${WIZARD_HEADER} ${RCC_SRCS} - ${MOC_SRCS} ${UI_HDRS} ) @@ -125,3 +106,7 @@ endif() if (WIN32) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION ".") endif(WIN32) + +if(USE_QT) + set_property(TARGET openmw-wizard PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index 176a02d507..0adc185b38 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -80,10 +80,6 @@ foreach (f ${ALL}) list (APPEND files "${f}") list (APPEND COMPONENT_QT_FILES "${f}") endforeach (f) -file (GLOB MOC_H "${dir}/${u}.hpp") -foreach (fi ${MOC_H}) -list (APPEND COMPONENT_MOC_FILES "${fi}") -endforeach (fi) endforeach (u) source_group ("components\\${dir}" FILES ${files}) endmacro (add_component_qt_dir) @@ -97,44 +93,21 @@ add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") endmacro (add_unit) -macro (add_qt_unit project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") -endmacro (add_qt_unit) - macro (add_hdr project dir unit) add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") endmacro (add_hdr) -macro (add_qt_hdr project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -endmacro (add_qt_hdr) - macro (opencs_units dir) foreach (u ${ARGN}) -add_qt_unit (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_units) - -macro (opencs_units_noqt dir) -foreach (u ${ARGN}) add_unit (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_units_noqt) +endmacro (opencs_units) macro (opencs_hdrs dir) foreach (u ${ARGN}) -add_qt_hdr (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_hdrs) - -macro (opencs_hdrs_noqt dir) -foreach (u ${ARGN}) add_hdr (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_hdrs_noqt) +endmacro (opencs_hdrs) include(CMakeParseArguments) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index ccac8f8cb7..b9db26dbc1 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -226,7 +226,6 @@ if (USE_QT) ) QT5_WRAP_UI(ESM_UI_HDR ${ESM_UI}) - QT5_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) endif() if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -280,7 +279,7 @@ if (WIN32) endif() if (USE_QT) - add_library(components_qt STATIC ${COMPONENT_QT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) + add_library(components_qt STATIC ${COMPONENT_QT_FILES} ${ESM_UI_HDR}) target_link_libraries(components_qt components Qt5::Widgets Qt5::Core) target_compile_definitions(components_qt PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") endif() @@ -289,6 +288,15 @@ if (GIT_CHECKOUT) add_dependencies (components git-version) endif (GIT_CHECKOUT) +if (OSG_STATIC AND CMAKE_SYSTEM_NAME MATCHES "Linux") + find_package(X11 REQUIRED COMPONENTS Xinerama Xrandr) + target_link_libraries(components ${CMAKE_DL_LIBS} X11::X11 X11::Xinerama X11::Xrandr) + find_package(Fontconfig MODULE) + if(Fontconfig_FOUND) + target_link_libraries(components Fontconfig::Fontconfig) + endif() +endif() + if (WIN32) target_link_libraries(components shlwapi) endif() @@ -330,6 +338,13 @@ if(OSG_STATIC) if(OPENMW_USE_SYSTEM_OSG) # OSG plugin pkgconfig files are missing these dependencies. # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(components freetype jpeg png) + find_package(Freetype REQUIRED) + find_package(JPEG REQUIRED) + find_package(PNG REQUIRED) + target_link_libraries(components Freetype::Freetype JPEG::JPEG PNG::PNG) endif() endif(OSG_STATIC) + +if(USE_QT) + set_property(TARGET components_qt PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index fb7ca7aac4..2d525e6f8c 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -353,15 +353,21 @@ namespace Compiler { if (const Extensions *extensions = getContext().getExtensions()) { + char returnType; // ignored std::string argumentType; // ignored bool hasExplicit = false; // ignored - if (extensions->isInstruction (keyword, argumentType, hasExplicit)) + bool isInstruction = extensions->isInstruction (keyword, argumentType, hasExplicit); + + if(isInstruction || (mExplicit.empty() && extensions->isFunction(keyword, returnType, argumentType, hasExplicit))) { - // pretend this is not a keyword std::string name = loc.mLiteral; if (name.size()>=2 && name[0]=='"' && name[name.size()-1]=='"') name = name.substr (1, name.size()-2); - return parseName (name, loc, scanner); + if(isInstruction || mLocals.getType(Misc::StringUtils::lowerCase(name)) != ' ') + { + // pretend this is not a keyword + return parseName (name, loc, scanner); + } } } @@ -421,42 +427,26 @@ namespace Compiler if (mNextOperand) { - if (keyword==Scanner::K_getsquareroot) + // check for custom extensions + if (const Extensions *extensions = getContext().getExtensions()) { start(); - mTokenLoc = loc; - parseArguments ("f", scanner); + char returnType; + std::string argumentType; - Generator::squareRoot (mCode); - mOperands.push_back ('f'); + bool hasExplicit = false; - mNextOperand = false; - return true; - } - else - { - // check for custom extensions - if (const Extensions *extensions = getContext().getExtensions()) + if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) { - start(); + mTokenLoc = loc; + int optionals = parseArguments (argumentType, scanner); - char returnType; - std::string argumentType; + extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); + mOperands.push_back (returnType); - bool hasExplicit = false; - - if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) - { - mTokenLoc = loc; - int optionals = parseArguments (argumentType, scanner); - - extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); - mOperands.push_back (returnType); - - mNextOperand = false; - return true; - } + mNextOperand = false; + return true; } } } diff --git a/components/compiler/generator.cpp b/components/compiler/generator.cpp index 34da1c4120..3d8b87e69e 100644 --- a/components/compiler/generator.cpp +++ b/components/compiler/generator.cpp @@ -104,11 +104,6 @@ namespace code.push_back (Compiler::Generator::segment5 (17)); } - void opSquareRoot (Compiler::Generator::CodeContainer& code) - { - code.push_back (Compiler::Generator::segment5 (19)); - } - void opReturn (Compiler::Generator::CodeContainer& code) { code.push_back (Compiler::Generator::segment5 (20)); @@ -452,11 +447,6 @@ namespace Compiler::Generator } } - void squareRoot (CodeContainer& code) - { - opSquareRoot (code); - } - void exit (CodeContainer& code) { opReturn (code); diff --git a/components/compiler/generator.hpp b/components/compiler/generator.hpp index 55bba2a75c..00c6e56825 100644 --- a/components/compiler/generator.hpp +++ b/components/compiler/generator.hpp @@ -74,8 +74,6 @@ namespace Compiler void convert (CodeContainer& code, char fromType, char toType); - void squareRoot (CodeContainer& code); - void exit (CodeContainer& code); void message (CodeContainer& code, Literals& literals, const std::string& message, diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 829df35be5..1054e2e269 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -164,8 +164,6 @@ namespace Compiler std::string value; c.appendTo(value); - bool error = false; - while (get (c)) { if (c.isDigit()) @@ -174,16 +172,11 @@ namespace Compiler } else if (!c.isMinusSign() && isStringCharacter (c)) { - error = true; - c.appendTo(value); + /// workaround that allows names to begin with digits + return scanName(c, parser, cont, value); } else if (c=='.') { - if (error) - { - putback (c); - break; - } return scanFloat (value, parser, cont); } else @@ -193,17 +186,6 @@ namespace Compiler } } - if (error) - { - /// workaround that allows names to begin with digits - /// \todo disable - TokenLoc loc (mLoc); - mLoc.mLiteral.clear(); - cont = parser.parseName (value, loc, *this); - return true; -// return false; - } - TokenLoc loc (mLoc); mLoc.mLiteral.clear(); @@ -265,13 +247,11 @@ namespace Compiler "return", "messagebox", "set", "to", - "getsquareroot", nullptr }; - bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont) + bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont, std::string name) { - std::string name; c.appendTo(name); if (!scanName (name)) diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 15781f9240..8ee2672132 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -205,8 +205,7 @@ namespace Compiler K_while, K_endwhile, K_return, K_messagebox, - K_set, K_to, - K_getsquareroot + K_set, K_to }; enum special @@ -237,7 +236,7 @@ namespace Compiler bool scanFloat (const std::string& intValue, Parser& parser, bool& cont); - bool scanName (MultiChar& c, Parser& parser, bool& cont); + bool scanName (MultiChar& c, Parser& parser, bool& cont, std::string name = {}); /// \param name May contain the start of the name (one or more characters) bool scanName (std::string& name); diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index 8b20377e1f..d9c3c04947 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -63,7 +63,7 @@ namespace Compiler keyword==Scanner::K_elseif || keyword==Scanner::K_while || keyword==Scanner::K_endwhile || keyword==Scanner::K_return || keyword==Scanner::K_messagebox || keyword==Scanner::K_set || - keyword==Scanner::K_to || keyword==Scanner::K_getsquareroot) + keyword==Scanner::K_to) { // pretend this is not a keyword std::string name = loc.mLiteral; diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 141ad13517..690f968142 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -107,34 +107,28 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index // addon can be checked if its gamefile is // ... special case, addon with no dependency can be used with any gamefile. - bool gamefileChecked = (file->gameFiles().count() == 0); + bool gamefileChecked = false; + bool noGameFiles = true; for (const QString &fileName : file->gameFiles()) { for (QListIterator dependencyIter(mFiles); dependencyIter.hasNext(); dependencyIter.next()) { //compare filenames only. Multiple instances //of the filename (with different paths) is not relevant here. - bool depFound = (dependencyIter.peekNext()->fileName().compare(fileName, Qt::CaseInsensitive) == 0); - - if (!depFound) + EsmFile* depFile = dependencyIter.peekNext(); + if (!depFile->isGameFile() || depFile->fileName().compare(fileName, Qt::CaseInsensitive) != 0) continue; - if (!gamefileChecked) + noGameFiles = false; + if (isChecked(depFile->filePath())) { - if (isChecked (dependencyIter.peekNext()->filePath())) - gamefileChecked = (dependencyIter.peekNext()->isGameFile()); - } - - // force it to iterate all files in cases where the current - // dependency is a game file to ensure that a later duplicate - // game file is / is not checked. - // (i.e., break only if it's not a gamefile or the game file has been checked previously) - if (gamefileChecked || !(dependencyIter.peekNext()->isGameFile())) + gamefileChecked = true; break; + } } } - if (gamefileChecked) + if (gamefileChecked || noGameFiles) { returnFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 6bb8e6e2c7..d7996dfae3 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -29,14 +29,12 @@ void ContentSelectorView::ContentSelector::buildContentModel() void ContentSelectorView::ContentSelector::buildGameFileView() { - ui.gameFileView->setVisible (true); - - ui.gameFileView->setPlaceholderText(QString("Select a game file...")); + ui.gameFileView->addItem(""); + ui.gameFileView->setVisible(true); connect (ui.gameFileView, SIGNAL (currentIndexChanged(int)), this, SLOT (slotCurrentGameFileIndexChanged(int))); - ui.gameFileView->setCurrentIndex(-1); ui.gameFileView->setCurrentIndex(0); } @@ -108,7 +106,7 @@ void ContentSelectorView::ContentSelector::setProfileContent(const QStringList & void ContentSelectorView::ContentSelector::setGameFile(const QString &filename) { - int index = -1; + int index = 0; if (!filename.isEmpty()) { @@ -168,10 +166,11 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path) } } - if (ui.gameFileView->currentIndex() != -1) - ui.gameFileView->setCurrentIndex(-1); + if (ui.gameFileView->currentIndex() != 0) + ui.gameFileView->setCurrentIndex(0); mContentModel->uncheckAll(); + mContentModel->checkForLoadOrderErrors(); } void ContentSelectorView::ContentSelector::clearFiles() @@ -183,7 +182,7 @@ QString ContentSelectorView::ContentSelector::currentFile() const { QModelIndex currentIdx = ui.addonView->currentIndex(); - if (!currentIdx.isValid()) + if (!currentIdx.isValid() && ui.gameFileView->currentIndex() > 0) return ui.gameFileView->currentText(); QModelIndex idx = mContentModel->index(mAddonProxyModel->mapToSource(currentIdx).row(), 0, QModelIndex()); diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 1b50f1e5e8..cda68fa1b7 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -40,7 +40,7 @@ namespace ContentSelectorView void setGameFile (const QString &filename = QString("")); bool isGamefileSelected() const - { return ui.gameFileView->currentIndex() != -1; } + { return ui.gameFileView->currentIndex() > 0; } QWidget *uiWidget() const { return ui.contentGroupBox; } diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index 5a8488c8d0..750901c73e 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -39,8 +39,8 @@ namespace DetourNavigator if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) { const ObjectId avoidId(avoidShape); - CollisionShape collisionShape {shapes.mShapeInstance, *avoidShape}; - if (mNavMeshManager.addObject(avoidId, collisionShape, transform, AreaType_null)) + CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.addObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 19125e0a9b..73b731c247 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -49,9 +49,9 @@ namespace DetourNavigator std::vector uniqueVertices; uniqueVertices.reserve(3 * triangles.size()); - for (const RecastMeshTriangle& v : triangles) - for (const osg::Vec3f& v : v.mVertices) - uniqueVertices.push_back(v); + for (const RecastMeshTriangle& triangle : triangles) + for (const osg::Vec3f& vertex : triangle.mVertices) + uniqueVertices.push_back(vertex); std::sort(uniqueVertices.begin(), uniqueVertices.end()); uniqueVertices.erase(std::unique(uniqueVertices.begin(), uniqueVertices.end()), uniqueVertices.end()); @@ -61,15 +61,15 @@ namespace DetourNavigator std::vector areaTypes; areaTypes.reserve(triangles.size()); - for (const RecastMeshTriangle& v : triangles) + for (const RecastMeshTriangle& triangle : triangles) { - areaTypes.push_back(v.mAreaType); + areaTypes.push_back(triangle.mAreaType); - for (const osg::Vec3f& v : v.mVertices) + for (const osg::Vec3f& vertex : triangle.mVertices) { - const auto it = std::lower_bound(uniqueVertices.begin(), uniqueVertices.end(), v); + const auto it = std::lower_bound(uniqueVertices.begin(), uniqueVertices.end(), vertex); assert(it != uniqueVertices.end()); - assert(*it == v); + assert(*it == vertex); indices.push_back(static_cast(it - uniqueVertices.begin())); } } @@ -79,11 +79,11 @@ namespace DetourNavigator std::vector vertices; vertices.reserve(3 * uniqueVertices.size()); - for (const osg::Vec3f& v : uniqueVertices) + for (const osg::Vec3f& vertex : uniqueVertices) { - vertices.push_back(v.x() + shift.x()); - vertices.push_back(v.y() + shift.y()); - vertices.push_back(v.z() + shift.z()); + vertices.push_back(vertex.x() + shift.x()); + vertices.push_back(vertex.y() + shift.y()); + vertices.push_back(vertex.z() + shift.z()); } return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); diff --git a/components/esm/activespells.cpp b/components/esm/activespells.cpp index 4017a4933e..22f862b6e4 100644 --- a/components/esm/activespells.cpp +++ b/components/esm/activespells.cpp @@ -3,44 +3,67 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM +namespace { - - void ActiveSpells::save(ESMWriter &esm) const + void save(ESM::ESMWriter& esm, const std::vector& spells, const std::string& tag) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) + for (const auto& params : spells) { - esm.writeHNString ("ID__", it->first); - - const ActiveSpellParams& params = it->second; + esm.writeHNString (tag, params.mId); esm.writeHNT ("CAST", params.mCasterActorId); esm.writeHNString ("DISP", params.mDisplayName); - - for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) + esm.writeHNT ("TYPE", params.mType); + if(params.mItem.isSet()) + params.mItem.save(esm, true, "ITEM"); + if(params.mWorsenings >= 0) { - esm.writeHNT ("MGEF", effectIt->mEffectId); - if (effectIt->mArg != -1) - esm.writeHNT ("ARG_", effectIt->mArg); - esm.writeHNT ("MAGN", effectIt->mMagnitude); - esm.writeHNT ("DURA", effectIt->mDuration); - esm.writeHNT ("EIND", effectIt->mEffectIndex); - esm.writeHNT ("LEFT", effectIt->mTimeLeft); + esm.writeHNT ("WORS", params.mWorsenings); + esm.writeHNT ("TIME", params.mNextWorsening); + } + + for (auto effect : params.mEffects) + { + esm.writeHNT ("MGEF", effect.mEffectId); + if (effect.mArg != -1) + esm.writeHNT ("ARG_", effect.mArg); + esm.writeHNT ("MAGN", effect.mMagnitude); + esm.writeHNT ("MAGN", effect.mMinMagnitude); + esm.writeHNT ("MAGN", effect.mMaxMagnitude); + esm.writeHNT ("DURA", effect.mDuration); + esm.writeHNT ("EIND", effect.mEffectIndex); + esm.writeHNT ("LEFT", effect.mTimeLeft); + esm.writeHNT ("FLAG", effect.mFlags); } } } - void ActiveSpells::load(ESMReader &esm) + void load(ESM::ESMReader& esm, std::vector& spells, const char* tag) { int format = esm.getFormat(); - while (esm.isNextSub("ID__")) + while (esm.isNextSub(tag)) { - std::string spellId = esm.getHString(); - - ActiveSpellParams params; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = esm.getHString(); esm.getHNT (params.mCasterActorId, "CAST"); params.mDisplayName = esm.getHNString ("DISP"); + params.mItem.unset(); + if (format < 17) + params.mType = ESM::ActiveSpells::Type_Temporary; + else + { + esm.getHNT (params.mType, "TYPE"); + if(esm.peekNextSub("ITEM")) + params.mItem.load(esm, true, "ITEM"); + } + if(esm.isNextSub("WORS")) + { + esm.getHT(params.mWorsenings); + esm.getHNT(params.mNextWorsening, "TIME"); + } + else + params.mWorsenings = -1; // spell casting timestamp, no longer used if (esm.isNextSub("TIME")) @@ -48,11 +71,21 @@ namespace ESM while (esm.isNextSub("MGEF")) { - ActiveEffect effect; + ESM::ActiveEffect effect; esm.getHT(effect.mEffectId); effect.mArg = -1; esm.getHNOT(effect.mArg, "ARG_"); esm.getHNT (effect.mMagnitude, "MAGN"); + if (format < 17) + { + effect.mMinMagnitude = effect.mMagnitude; + effect.mMaxMagnitude = effect.mMagnitude; + } + else + { + esm.getHNT (effect.mMinMagnitude, "MAGN"); + esm.getHNT (effect.mMaxMagnitude, "MAGN"); + } esm.getHNT (effect.mDuration, "DURA"); effect.mEffectIndex = -1; esm.getHNOT (effect.mEffectIndex, "EIND"); @@ -60,10 +93,30 @@ namespace ESM effect.mTimeLeft = effect.mDuration; else esm.getHNT (effect.mTimeLeft, "LEFT"); + if (format < 17) + effect.mFlags = ESM::ActiveEffect::Flag_None; + else + esm.getHNT (effect.mFlags, "FLAG"); params.mEffects.push_back(effect); } - mSpells.insert(std::make_pair(spellId, params)); + spells.emplace_back(params); } } } + +namespace ESM +{ + + void ActiveSpells::save(ESMWriter &esm) const + { + ::save(esm, mSpells, "ID__"); + ::save(esm, mQueue, "QID_"); + } + + void ActiveSpells::load(ESMReader &esm) + { + ::load(esm, mSpells, "ID__"); + ::load(esm, mQueue, "QID_"); + } +} diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 1b7f8b319c..8b5f1f1946 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -1,11 +1,12 @@ #ifndef OPENMW_ESM_ACTIVESPELLS_H #define OPENMW_ESM_ACTIVESPELLS_H -#include "effectlist.hpp" +#include "cellref.hpp" #include "defs.hpp" +#include "effectlist.hpp" #include -#include +#include namespace ESM { @@ -14,29 +15,53 @@ namespace ESM // Parameters of an effect concerning lasting effects. // Note we are not using ENAMstruct since the magnitude may be modified by magic resistance, etc. - // It could also be a negative magnitude, in case of inversing an effect, e.g. Absorb spell causes damage on target, but heals the caster. struct ActiveEffect { + enum Flags + { + Flag_None = 0, + Flag_Applied = 1 << 0, + Flag_Remove = 1 << 1, + Flag_Ignore_Resistances = 1 << 2 + }; + int mEffectId; float mMagnitude; + float mMinMagnitude; + float mMaxMagnitude; int mArg; // skill or attribute float mDuration; float mTimeLeft; int mEffectIndex; + int mFlags; }; // format 0, saved games only struct ActiveSpells { + enum EffectType + { + Type_Temporary, + Type_Ability, + Type_Enchantment, + Type_Permanent, + Type_Consumable + }; + struct ActiveSpellParams { + std::string mId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; + RefNum mItem; + EffectType mType; + int mWorsenings; + TimeStamp mNextWorsening; }; - typedef std::multimap TContainer; - TContainer mSpells; + std::vector mSpells; + std::vector mQueue; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index cb383992c6..d5030a6580 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -110,16 +110,31 @@ void ESM::CreatureStats::load (ESMReader &esm) mAiSequence.load(esm); mMagicEffects.load(esm); - while (esm.isNextSub("SUMM")) + if (esm.getFormat() < 17) { - int magicEffect; - esm.getHT(magicEffect); - std::string source = esm.getHNOString("SOUR"); - int effectIndex = -1; - esm.getHNOT (effectIndex, "EIND"); - int actorId; - esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + std::string source = esm.getHNOString("SOUR"); + int effectIndex = -1; + esm.getHNOT (effectIndex, "EIND"); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + mSummonedCreatures.emplace(magicEffect, actorId); + } + } + else + { + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatures.emplace(magicEffect, actorId); + } } while (esm.isNextSub("GRAV")) @@ -214,14 +229,10 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSequence.save(esm); mMagicEffects.save(esm); - for (const auto& summon : mSummonedCreatureMap) + for (const auto& [effectId, actorId] : mSummonedCreatures) { - esm.writeHNT ("SUMM", summon.first.mEffectId); - esm.writeHNString ("SOUR", summon.first.mSourceId); - int effectIndex = summon.first.mEffectIndex; - if (effectIndex != -1) - esm.writeHNT ("EIND", effectIndex); - esm.writeHNT ("ACID", summon.second); + esm.writeHNT ("SUMM", effectId); + esm.writeHNT ("ACID", actorId); } for (int key : mSummonGraveyard) @@ -235,15 +246,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const for (int i=0; i<4; ++i) mAiSettings[i].save(esm); } - - for (const auto& corprusSpell : mCorprusSpells) - { - esm.writeHNString("CORP", corprusSpell.first); - - const CorprusStats & stats = corprusSpell.second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } } void ESM::CreatureStats::blank() diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 13bc50008c..651b126d0e 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -40,6 +40,7 @@ namespace ESM StatState mAiSettings[4]; std::map mSummonedCreatureMap; + std::multimap mSummonedCreatures; std::vector mSummonGraveyard; ESM::TimeStamp mTradeTime; diff --git a/components/esm/debugprofile.cpp b/components/esm/debugprofile.cpp index 1dcf661fc9..6276258c48 100644 --- a/components/esm/debugprofile.cpp +++ b/components/esm/debugprofile.cpp @@ -9,6 +9,7 @@ unsigned int ESM::DebugProfile::sRecordId = REC_DBGP; void ESM::DebugProfile::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { diff --git a/components/esm/debugprofile.hpp b/components/esm/debugprofile.hpp index c056750a88..8340404c23 100644 --- a/components/esm/debugprofile.hpp +++ b/components/esm/debugprofile.hpp @@ -19,6 +19,7 @@ namespace ESM Flag_Global = 4 // make available from main menu (i.e. not location specific) }; + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/filter.cpp b/components/esm/filter.cpp index 5bae1ed03e..8d1b755055 100644 --- a/components/esm/filter.cpp +++ b/components/esm/filter.cpp @@ -9,6 +9,7 @@ unsigned int ESM::Filter::sRecordId = REC_FILT; void ESM::Filter::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { diff --git a/components/esm/filter.hpp b/components/esm/filter.hpp index b1c511ebba..78d51cec00 100644 --- a/components/esm/filter.hpp +++ b/components/esm/filter.hpp @@ -12,6 +12,7 @@ namespace ESM { static unsigned int sRecordId; + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/loadbody.cpp b/components/esm/loadbody.cpp index 4ddefc92c7..239cff7c8b 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm/loadbody.cpp @@ -11,6 +11,7 @@ namespace ESM void BodyPart::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadbody.hpp b/components/esm/loadbody.hpp index bf320330ff..1be775ffec 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm/loadbody.hpp @@ -58,6 +58,7 @@ struct BodyPart }; BYDTstruct mData; + unsigned int mRecordFlags; std::string mId, mModel, mRace; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadbsgn.cpp b/components/esm/loadbsgn.cpp index 1f679af39a..7514f1f85b 100644 --- a/components/esm/loadbsgn.cpp +++ b/components/esm/loadbsgn.cpp @@ -11,6 +11,7 @@ namespace ESM void BirthSign::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); diff --git a/components/esm/loadbsgn.hpp b/components/esm/loadbsgn.hpp index 24d27a7f85..806323bf35 100644 --- a/components/esm/loadbsgn.hpp +++ b/components/esm/loadbsgn.hpp @@ -17,6 +17,7 @@ struct BirthSign /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "BirthSign"; } + unsigned int mRecordFlags; std::string mId, mName, mDescription, mTexture; // List of powers and abilities that come with this birth sign. diff --git a/components/esm/loadclas.cpp b/components/esm/loadclas.cpp index b76fc57067..7526fe4f52 100644 --- a/components/esm/loadclas.cpp +++ b/components/esm/loadclas.cpp @@ -41,6 +41,7 @@ namespace ESM void Class::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadclas.hpp b/components/esm/loadclas.hpp index 833dd6757d..1000879c4c 100644 --- a/components/esm/loadclas.hpp +++ b/components/esm/loadclas.hpp @@ -70,6 +70,7 @@ struct Class ///< Throws an exception for invalid values of \a index. }; // 60 bytes + unsigned int mRecordFlags; std::string mId, mName, mDescription; CLDTstruct mData; diff --git a/components/esm/loadench.cpp b/components/esm/loadench.cpp index 3c1a2f1eda..ed3de90b50 100644 --- a/components/esm/loadench.cpp +++ b/components/esm/loadench.cpp @@ -11,6 +11,7 @@ namespace ESM void Enchantment::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); bool hasName = false; diff --git a/components/esm/loadench.hpp b/components/esm/loadench.hpp index b98549ef35..a4e1e8362c 100644 --- a/components/esm/loadench.hpp +++ b/components/esm/loadench.hpp @@ -42,6 +42,7 @@ struct Enchantment int mFlags; }; + unsigned int mRecordFlags; std::string mId; ENDTstruct mData; EffectList mEffects; diff --git a/components/esm/loadfact.cpp b/components/esm/loadfact.cpp index bd0962721b..b71348de44 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm/loadfact.cpp @@ -29,6 +29,7 @@ namespace ESM void Faction::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mReactions.clear(); for (int i=0;i<10;++i) diff --git a/components/esm/loadfact.hpp b/components/esm/loadfact.hpp index 098ed43096..6a42377901 100644 --- a/components/esm/loadfact.hpp +++ b/components/esm/loadfact.hpp @@ -34,6 +34,7 @@ struct Faction /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Faction"; } + unsigned int mRecordFlags; std::string mId, mName; struct FADTstruct diff --git a/components/esm/loadglob.cpp b/components/esm/loadglob.cpp index 72ecce503c..d2226d1738 100644 --- a/components/esm/loadglob.cpp +++ b/components/esm/loadglob.cpp @@ -11,6 +11,7 @@ namespace ESM void Global::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString ("NAME"); diff --git a/components/esm/loadglob.hpp b/components/esm/loadglob.hpp index 0533cc95ea..9dd58e6c67 100644 --- a/components/esm/loadglob.hpp +++ b/components/esm/loadglob.hpp @@ -21,6 +21,7 @@ struct Global /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Global"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadgmst.cpp b/components/esm/loadgmst.cpp index da8d256e7d..6d4ac1b202 100644 --- a/components/esm/loadgmst.cpp +++ b/components/esm/loadgmst.cpp @@ -11,6 +11,7 @@ namespace ESM void GameSetting::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; // GameSetting record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString("NAME"); mValue.read (esm, ESM::Variant::Format_Gmst); diff --git a/components/esm/loadgmst.hpp b/components/esm/loadgmst.hpp index c40d348fe4..931ee286a4 100644 --- a/components/esm/loadgmst.hpp +++ b/components/esm/loadgmst.hpp @@ -22,6 +22,7 @@ struct GameSetting /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "GameSetting"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp index 75a94f828a..4bc09920e9 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm/loadmgef.cpp @@ -189,6 +189,7 @@ namespace ESM void MagicEffect::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // MagicEffect record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); esm.getHNT(mIndex, "INDX"); diff --git a/components/esm/loadmgef.hpp b/components/esm/loadmgef.hpp index d718aaccf5..480478d81e 100644 --- a/components/esm/loadmgef.hpp +++ b/components/esm/loadmgef.hpp @@ -16,6 +16,7 @@ struct MagicEffect /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "MagicEffect"; } + unsigned int mRecordFlags; std::string mId; enum Flags diff --git a/components/esm/loadrace.cpp b/components/esm/loadrace.cpp index ce3cc95bf8..44dbde7742 100644 --- a/components/esm/loadrace.cpp +++ b/components/esm/loadrace.cpp @@ -21,6 +21,7 @@ namespace ESM void Race::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); diff --git a/components/esm/loadrace.hpp b/components/esm/loadrace.hpp index d014744472..50fa73ad74 100644 --- a/components/esm/loadrace.hpp +++ b/components/esm/loadrace.hpp @@ -65,6 +65,7 @@ struct Race RADTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mDescription; SpellList mPowers; diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index 91ea92e305..e39887a1b1 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -11,6 +11,7 @@ namespace ESM void Region::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; while (esm.hasMoreSubs()) diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index 6f39dc0bff..74f6b123ec 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -45,6 +45,7 @@ struct Region WEATstruct mData; int mMapColor; // RGBA + unsigned int mRecordFlags; // sleepList refers to a leveled list of creatures you can meet if // you sleep outside in this region. std::string mId, mName, mSleepList; diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 19602fef62..8715c83dcb 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -83,6 +83,7 @@ namespace ESM void Script::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mVarNames.clear(); diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index e1ffe1b864..d518a048ff 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -35,6 +35,7 @@ public: Script::SCHDstruct mData; }; + unsigned int mRecordFlags; std::string mId; SCHDstruct mData; diff --git a/components/esm/loadskil.cpp b/components/esm/loadskil.cpp index 61cca7d0d7..9f58176f35 100644 --- a/components/esm/loadskil.cpp +++ b/components/esm/loadskil.cpp @@ -130,6 +130,7 @@ namespace ESM void Skill::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // Skill record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); bool hasIndex = false; bool hasData = false; diff --git a/components/esm/loadskil.hpp b/components/esm/loadskil.hpp index 099264fab7..ae44a51045 100644 --- a/components/esm/loadskil.hpp +++ b/components/esm/loadskil.hpp @@ -22,6 +22,7 @@ struct Skill /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Skill"; } + unsigned int mRecordFlags; std::string mId; struct SKDTstruct diff --git a/components/esm/loadsndg.cpp b/components/esm/loadsndg.cpp index c6ea930a20..c439d0ca66 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm/loadsndg.cpp @@ -11,6 +11,7 @@ namespace ESM void SoundGenerator::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadsndg.hpp b/components/esm/loadsndg.hpp index 70b221e98c..99aae06e0e 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm/loadsndg.hpp @@ -34,6 +34,7 @@ struct SoundGenerator // Type int mType; + unsigned int mRecordFlags; std::string mId, mCreature, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadsoun.cpp b/components/esm/loadsoun.cpp index ccb5f6fdce..ed8fc519a6 100644 --- a/components/esm/loadsoun.cpp +++ b/components/esm/loadsoun.cpp @@ -11,6 +11,7 @@ namespace ESM void Sound::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadsoun.hpp b/components/esm/loadsoun.hpp index 937e22be88..14f1178650 100644 --- a/components/esm/loadsoun.hpp +++ b/components/esm/loadsoun.hpp @@ -21,6 +21,7 @@ struct Sound static std::string getRecordType() { return "Sound"; } SOUNstruct mData; + unsigned int mRecordFlags; std::string mId, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadspel.cpp b/components/esm/loadspel.cpp index 34e146501c..5983cdcdf5 100644 --- a/components/esm/loadspel.cpp +++ b/components/esm/loadspel.cpp @@ -11,6 +11,7 @@ namespace ESM void Spell::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); diff --git a/components/esm/loadspel.hpp b/components/esm/loadspel.hpp index 1763d0991c..ef74c2c312 100644 --- a/components/esm/loadspel.hpp +++ b/components/esm/loadspel.hpp @@ -42,6 +42,7 @@ struct Spell }; SPDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName; EffectList mEffects; diff --git a/components/esm/loadsscr.cpp b/components/esm/loadsscr.cpp index f436c32a1c..9e060ab1a7 100644 --- a/components/esm/loadsscr.cpp +++ b/components/esm/loadsscr.cpp @@ -11,6 +11,7 @@ namespace ESM void StartScript::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasData = false; bool hasName = false; diff --git a/components/esm/loadsscr.hpp b/components/esm/loadsscr.hpp index ce2ff49e77..3e84027076 100644 --- a/components/esm/loadsscr.hpp +++ b/components/esm/loadsscr.hpp @@ -24,6 +24,7 @@ struct StartScript static std::string getRecordType() { return "StartScript"; } std::string mData; + unsigned int mRecordFlags; std::string mId; // Load a record and add it to the list diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp index 898e7e4b18..a1f943a93d 100644 --- a/components/esm/magiceffects.cpp +++ b/components/esm/magiceffects.cpp @@ -8,10 +8,11 @@ namespace ESM void MagicEffects::save(ESMWriter &esm) const { - for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + for (const auto& [key, params] : mEffects) { - esm.writeHNT("EFID", it->first); - esm.writeHNT("BASE", it->second); + esm.writeHNT("EFID", key); + esm.writeHNT("BASE", params.first); + esm.writeHNT("MODI", params.second); } } @@ -19,10 +20,15 @@ void MagicEffects::load(ESMReader &esm) { while (esm.isNextSub("EFID")) { - int id, base; + int id; + std::pair params; esm.getHT(id); - esm.getHNT(base, "BASE"); - mEffects.insert(std::make_pair(id, base)); + esm.getHNT(params.first, "BASE"); + if(esm.getFormat() < 17) + params.second = 0.f; + else + esm.getHNT(params.second, "MODI"); + mEffects.emplace(id, params); } } diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp index 5b8b0c924a..4b54692c5f 100644 --- a/components/esm/magiceffects.hpp +++ b/components/esm/magiceffects.hpp @@ -12,8 +12,8 @@ namespace ESM // format 0, saved games only struct MagicEffects { - // - std::map mEffects; + // + std::map> mEffects; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/projectilestate.cpp b/components/esm/projectilestate.cpp index 8ade9d5b2e..3421c19526 100644 --- a/components/esm/projectilestate.cpp +++ b/components/esm/projectilestate.cpp @@ -28,6 +28,7 @@ namespace ESM esm.writeHNString ("SPEL", mSpellId); esm.writeHNT ("SPED", mSpeed); + esm.writeHNT ("SLOT", mSlot); } void MagicBoltState::load(ESMReader &esm) @@ -39,6 +40,10 @@ namespace ESM esm.skipHSub(); ESM::EffectList().load(esm); // for backwards compatibility esm.getHNT (mSpeed, "SPED"); + if(esm.getFormat() < 17) + mSlot = 0; + else + esm.getHNT(mSlot, "SLOT"); if (esm.isNextSub("STCK")) // for backwards compatibility esm.skipHSub(); if (esm.isNextSub("SOUN")) // for backwards compatibility diff --git a/components/esm/projectilestate.hpp b/components/esm/projectilestate.hpp index 67ec89bb6d..84292813ce 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm/projectilestate.hpp @@ -32,6 +32,7 @@ namespace ESM { std::string mSpellId; float mSpeed; + int mSlot; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3f8bf10c56..8a98a63419 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -4,7 +4,7 @@ #include "esmwriter.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 16; +int ESM::SavedGame::sCurrentFormat = 17; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/esm/spellstate.cpp b/components/esm/spellstate.cpp index 2eb1e78679..b1ddb6523c 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm/spellstate.cpp @@ -8,29 +8,38 @@ namespace ESM void SpellState::load(ESMReader &esm) { - while (esm.isNextSub("SPEL")) + if(esm.getFormat() < 17) { - std::string id = esm.getHString(); - - SpellParams state; - while (esm.isNextSub("INDX")) + while (esm.isNextSub("SPEL")) { - int index; - esm.getHT(index); + std::string id = esm.getHString(); - float magnitude; - esm.getHNT(magnitude, "RAND"); + SpellParams state; + while (esm.isNextSub("INDX")) + { + int index; + esm.getHT(index); - state.mEffectRands[index] = magnitude; + float magnitude; + esm.getHNT(magnitude, "RAND"); + + state.mEffectRands[index] = magnitude; + } + + while (esm.isNextSub("PURG")) { + int index; + esm.getHT(index); + state.mPurgedEffects.insert(index); + } + + mSpellParams[id] = state; + mSpells.emplace_back(id); } - - while (esm.isNextSub("PURG")) { - int index; - esm.getHT(index); - state.mPurgedEffects.insert(index); - } - - mSpells[id] = state; + } + else + { + while (esm.isNextSub("SPEL")) + mSpells.emplace_back(esm.getHString()); } // Obsolete @@ -88,30 +97,8 @@ namespace ESM void SpellState::save(ESMWriter &esm) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - esm.writeHNString("SPEL", it->first); - - const std::map& random = it->second.mEffectRands; - for (std::map::const_iterator rIt = random.begin(); rIt != random.end(); ++rIt) - { - esm.writeHNT("INDX", rIt->first); - esm.writeHNT("RAND", rIt->second); - } - - const std::set& purges = it->second.mPurgedEffects; - for (std::set::const_iterator pIt = purges.begin(); pIt != purges.end(); ++pIt) - esm.writeHNT("PURG", *pIt); - } - - for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - esm.writeHNString("CORP", it->first); - - const CorprusStats & stats = it->second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } + for (const std::string& spell : mSpells) + esm.writeHNString("SPEL", spell); for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { diff --git a/components/esm/spellstate.hpp b/components/esm/spellstate.hpp index 55c57611a2..e7067dae8c 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm/spellstate.hpp @@ -31,13 +31,13 @@ namespace ESM struct SpellParams { - std::map mEffectRands; - std::set mPurgedEffects; + std::map mEffectRands; // + std::set mPurgedEffects; // indices of purged effects }; - typedef std::map TContainer; - TContainer mSpells; + std::vector mSpells; // FIXME: obsolete, used only for old saves + std::map mSpellParams; std::map > mPermanentSpellEffects; std::map mCorprusSpells; diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 8aa6853237..dad52a7ee4 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -83,7 +83,8 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m mSilent = silent; } -boost::program_options::variables_map ConfigurationManager::separateComposingVariables(boost::program_options::variables_map & variables, boost::program_options::options_description& description) +boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map & variables, + boost::program_options::options_description& description) { boost::program_options::variables_map composingVariables; for (auto itr = variables.begin(); itr != variables.end();) @@ -99,7 +100,8 @@ boost::program_options::variables_map ConfigurationManager::separateComposingVar return composingVariables; } -void ConfigurationManager::mergeComposingVariables(boost::program_options::variables_map & first, boost::program_options::variables_map & second, boost::program_options::options_description& description) +void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, + boost::program_options::options_description& description) { // There are a few places this assumes all variables are present in second, but it's never crashed in the wild, so it looks like that's guaranteed. std::set replacedVariables; @@ -235,10 +237,10 @@ bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, Log(Debug::Info) << "Loading config file: " << cfgFile.string(); boost::filesystem::ifstream configFileStream(cfgFile); + if (configFileStream.is_open()) { - boost::program_options::store(Files::parse_config_file( - configFileStream, description, true), variables); + parseConfig(configFileStream, variables, description); return true; } @@ -298,4 +300,22 @@ const boost::filesystem::path& ConfigurationManager::getScreenshotPath() const return mScreenshotPath; } +void parseArgs(int argc, const char* const argv[], boost::program_options::variables_map& variables, + boost::program_options::options_description& description) +{ + boost::program_options::store( + boost::program_options::command_line_parser(argc, argv).options(description).allow_unregistered().run(), + variables + ); +} + +void parseConfig(std::istream& stream, boost::program_options::variables_map& variables, + boost::program_options::options_description& description) +{ + boost::program_options::store( + Files::parse_config_file(stream, description, true), + variables + ); +} + } /* namespace Cfg */ diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 0ec0a1f67d..99e65a7658 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -25,10 +25,6 @@ struct ConfigurationManager void readConfiguration(boost::program_options::variables_map& variables, boost::program_options::options_description& description, bool quiet=false); - boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map& variables, boost::program_options::options_description& description); - - void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, boost::program_options::options_description& description); - void processPaths(Files::PathContainer& dataDirs, bool create = false); ///< \param create Try creating the directory, if it does not exist. @@ -68,6 +64,19 @@ struct ConfigurationManager bool mSilent; }; + +boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map& variables, + boost::program_options::options_description& description); + +void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, + boost::program_options::options_description& description); + +void parseArgs(int argc, const char* const argv[], boost::program_options::variables_map& variables, + boost::program_options::options_description& description); + +void parseConfig(std::istream& stream, boost::program_options::variables_map& variables, + boost::program_options::options_description& description); + } /* namespace Cfg */ #endif /* COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP */ diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 487b7bd70b..da43cc38ec 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -17,6 +17,7 @@ #include +#include #include #include @@ -191,24 +192,10 @@ namespace Gui void FontLoader::loadBitmapFonts(bool exportToFile) { - const std::map& index = mVFS->getIndex(); - - std::string pattern = "Fonts/"; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& name : mVFS->getRecursiveDirectoryIterator("Fonts/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".fnt") == 0) - loadBitmapFont(name, exportToFile); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "fnt") + loadBitmapFont(name, exportToFile); } } diff --git a/components/interpreter/docs/vmformat.txt b/components/interpreter/docs/vmformat.txt index b5c9cf0ae0..7eac8b26e0 100644 --- a/components/interpreter/docs/vmformat.txt +++ b/components/interpreter/docs/vmformat.txt @@ -77,7 +77,7 @@ op 15: div (integer) stack[1] by stack[0], pop twice, push result op 16: div (float) stack[1] by stack[0], pop twice, push result op 17: convert stack[1] from integer to float op 18: convert stack[1] from float to integer -op 19: take square root of stack[0] (float) +opcode 19 unused op 20: return op 21: replace stack[0] with local short stack[0] op 22: replace stack[0] with local long stack[0] diff --git a/components/interpreter/installopcodes.cpp b/components/interpreter/installopcodes.cpp index afee36bc28..b5cb229e84 100644 --- a/components/interpreter/installopcodes.cpp +++ b/components/interpreter/installopcodes.cpp @@ -59,7 +59,6 @@ namespace Interpreter interpreter.installSegment5 (14, new OpMulInt); interpreter.installSegment5 (15, new OpDivInt); interpreter.installSegment5 (16, new OpDivInt); - interpreter.installSegment5 (19, new OpSquareRoot); interpreter.installSegment5 (26, new OpCompare >); interpreter.installSegment5 (27, diff --git a/components/interpreter/mathopcodes.hpp b/components/interpreter/mathopcodes.hpp index 42cb486b9c..bf580c6c2e 100644 --- a/components/interpreter/mathopcodes.hpp +++ b/components/interpreter/mathopcodes.hpp @@ -74,24 +74,6 @@ namespace Interpreter } }; - class OpSquareRoot : public Opcode0 - { - public: - - void execute (Runtime& runtime) override - { - Type_Float value = runtime[0].mFloat; - - if (value<0) - throw std::runtime_error ( - "square root of negative number (we aren't that imaginary)"); - - value = std::sqrt (value); - - runtime[0].mFloat = value; - } - }; - template class OpCompare : public Opcode0 { @@ -105,7 +87,7 @@ namespace Interpreter runtime[0].mInteger = result; } - }; + }; } #endif diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index fd42aa7fb2..8e4719dba4 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -67,27 +67,31 @@ namespace LuaUtil mSandboxEnv = sol::nil; } - sol::table LuaState::makeReadOnly(sol::table table) + sol::table makeReadOnly(sol::table table) { + if (table == sol::nil) + return table; if (table.is()) return table; // it is already userdata, no sense to wrap it again + lua_State* lua = table.lua_state(); table[sol::meta_function::index] = table; - sol::stack::push(mLua, std::move(table)); - lua_newuserdata(mLua, 0); - lua_pushvalue(mLua, -2); - lua_setmetatable(mLua, -2); - return sol::stack::pop(mLua); + sol::stack::push(lua, std::move(table)); + lua_newuserdata(lua, 0); + lua_pushvalue(lua, -2); + lua_setmetatable(lua, -2); + return sol::stack::pop(lua); } - sol::table LuaState::getMutableFromReadOnly(const sol::userdata& ro) + sol::table getMutableFromReadOnly(const sol::userdata& ro) { - sol::stack::push(mLua, ro); - int ok = lua_getmetatable(mLua, -1); + lua_State* lua = ro.lua_state(); + sol::stack::push(lua, ro); + int ok = lua_getmetatable(lua, -1); assert(ok); (void)ok; - sol::table res = sol::stack::pop(mLua); - lua_pop(mLua, 1); + sol::table res = sol::stack::pop(lua); + lua_pop(lua, 1); return res; } diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index acaaadea76..8982b49b36 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -3,7 +3,6 @@ #include -#include // missing from sol/sol.hpp #include #include @@ -37,11 +36,6 @@ namespace LuaUtil // A shortcut to create a new Lua table. sol::table newTable() { return sol::table(mLua, sol::create); } - // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. - // Needed to forbid any changes in common resources that can accessed from different sandboxes. - sol::table makeReadOnly(sol::table); - sol::table getMutableFromReadOnly(const sol::userdata&); - // Registers a package that will be available from every sandbox via `require(name)`. // The package can be either a sol::table with an API or a sol::function. If it is a function, // it will be evaluated (once per sandbox) the first time when requested. If the package @@ -106,6 +100,11 @@ namespace LuaUtil // String representation of a Lua object. Should be used for debugging/logging purposes only. std::string toString(const sol::object&); + // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. + // Needed to forbid any changes in common resources that can accessed from different sandboxes. + sol::table makeReadOnly(sol::table); + sol::table getMutableFromReadOnly(const sol::userdata&); + } #endif // COMPONENTS_LUA_LUASTATE_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index a53b1d0404..703381a453 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -16,6 +16,15 @@ namespace LuaUtil static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; + std::string ScriptsContainer::ScriptId::toString() const + { + std::string res = mContainer->mNamePrefix; + res.push_back('['); + res.append(mPath); + res.push_back(']'); + return res; + } + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) { registerEngineHandlers({&mUpdateHandlers}); @@ -25,7 +34,7 @@ namespace LuaUtil void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) { - API[packageName] = mLua.makeReadOnly(std::move(package)); + API[packageName] = makeReadOnly(std::move(package)); } bool ScriptsContainer::addNewScript(const std::string& path) @@ -63,7 +72,7 @@ namespace LuaUtil if (interfaceName.empty() != (publicInterface == sol::nil)) Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; else if (!interfaceName.empty()) - script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = mLua.makeReadOnly(publicInterface); + script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); mScriptOrder.push_back(path); mScripts[path].mInterface = std::move(script); return true; @@ -81,6 +90,7 @@ namespace LuaUtil auto scriptIter = mScripts.find(path); if (scriptIter == mScripts.end()) return false; // no such script + scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil; sol::object& script = scriptIter->second.mInterface; if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) { @@ -318,8 +328,16 @@ namespace LuaUtil std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } + ScriptsContainer::~ScriptsContainer() + { + for (auto& [_, script] : mScripts) + script.mHiddenData[ScriptId::KEY] = sol::nil; + } + void ScriptsContainer::removeAllScripts() { + for (auto& [_, script] : mScripts) + script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.clear(); mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) @@ -329,7 +347,7 @@ namespace LuaUtil mHoursTimersQueue.clear(); mPublicInterfaces.clear(); - // Assigned by mLua.makeReadOnly, but `clear` removes it, so we need to assign it again. + // Assigned by LuaUtil::makeReadOnly, but `clear` removes it, so we need to assign it again. mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 7b2b2a7aa9..69aa18e940 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -66,6 +66,8 @@ namespace LuaUtil ScriptsContainer* mContainer; std::string mPath; + + std::string toString() const; }; using TimeUnit = ESM::LuaTimer::TimeUnit; @@ -73,10 +75,10 @@ namespace LuaUtil ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; - virtual ~ScriptsContainer() {} + virtual ~ScriptsContainer(); // Adds package that will be available (via `require`) for all scripts in the container. - // Automatically applies LuaState::makeReadOnly to the package. + // Automatically applies LuaUtil::makeReadOnly to the package. void addPackage(const std::string& packageName, sol::object package); // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index abcc6d424e..17cb64461a 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -3,17 +3,23 @@ #include #include -#include - #include +#include "luastate.hpp" + namespace sol { template <> - struct is_automagical : std::false_type {}; + struct is_automagical : std::false_type {}; template <> - struct is_automagical : std::false_type {}; + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; } namespace LuaUtil @@ -23,70 +29,148 @@ namespace LuaUtil { sol::table util(lua, sol::create); - // TODO: Add bindings for osg::Matrix - - // Lua bindings for osg::Vec2f - util["vector2"] = [](float x, float y) { return osg::Vec2f(x, y); }; - sol::usertype vec2Type = lua.new_usertype("Vec2"); - vec2Type["x"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.x(); } ); - vec2Type["y"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.y(); } ); - vec2Type[sol::meta_function::to_string] = [](const osg::Vec2f& v) { + // Lua bindings for Vec2 + util["vector2"] = [](float x, float y) { return Vec2(x, y); }; + sol::usertype vec2Type = lua.new_usertype("Vec2"); + vec2Type["x"] = sol::readonly_property([](const Vec2& v) -> float { return v.x(); } ); + vec2Type["y"] = sol::readonly_property([](const Vec2& v) -> float { return v.y(); } ); + vec2Type[sol::meta_function::to_string] = [](const Vec2& v) { std::stringstream ss; ss << "(" << v.x() << ", " << v.y() << ")"; return ss.str(); }; - vec2Type[sol::meta_function::unary_minus] = [](const osg::Vec2f& a) { return -a; }; - vec2Type[sol::meta_function::addition] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a + b; }; - vec2Type[sol::meta_function::subtraction] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a - b; }; - vec2Type[sol::meta_function::equal_to] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a == b; }; + vec2Type[sol::meta_function::unary_minus] = [](const Vec2& a) { return -a; }; + vec2Type[sol::meta_function::addition] = [](const Vec2& a, const Vec2& b) { return a + b; }; + vec2Type[sol::meta_function::subtraction] = [](const Vec2& a, const Vec2& b) { return a - b; }; + vec2Type[sol::meta_function::equal_to] = [](const Vec2& a, const Vec2& b) { return a == b; }; vec2Type[sol::meta_function::multiplication] = sol::overload( - [](const osg::Vec2f& a, float c) { return a * c; }, - [](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; }); - vec2Type[sol::meta_function::division] = [](const osg::Vec2f& a, float c) { return a / c; }; - vec2Type["dot"] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; }; - vec2Type["length"] = &osg::Vec2f::length; - vec2Type["length2"] = &osg::Vec2f::length2; - vec2Type["normalize"] = [](const osg::Vec2f& v) { + [](const Vec2& a, float c) { return a * c; }, + [](const Vec2& a, const Vec2& b) { return a * b; }); + vec2Type[sol::meta_function::division] = [](const Vec2& a, float c) { return a / c; }; + vec2Type["dot"] = [](const Vec2& a, const Vec2& b) { return a * b; }; + vec2Type["length"] = &Vec2::length; + vec2Type["length2"] = &Vec2::length2; + vec2Type["normalize"] = [](const Vec2& v) { float len = v.length(); if (len == 0) - return std::make_tuple(osg::Vec2f(), 0.f); + return std::make_tuple(Vec2(), 0.f); else return std::make_tuple(v * (1.f / len), len); }; vec2Type["rotate"] = &Misc::rotateVec2f; - // Lua bindings for osg::Vec3f - util["vector3"] = [](float x, float y, float z) { return osg::Vec3f(x, y, z); }; - sol::usertype vec3Type = lua.new_usertype("Vec3"); - vec3Type["x"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.x(); } ); - vec3Type["y"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.y(); } ); - vec3Type["z"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.z(); } ); - vec3Type[sol::meta_function::to_string] = [](const osg::Vec3f& v) { + // Lua bindings for Vec3 + util["vector3"] = [](float x, float y, float z) { return Vec3(x, y, z); }; + sol::usertype vec3Type = lua.new_usertype("Vec3"); + vec3Type["x"] = sol::readonly_property([](const Vec3& v) -> float { return v.x(); } ); + vec3Type["y"] = sol::readonly_property([](const Vec3& v) -> float { return v.y(); } ); + vec3Type["z"] = sol::readonly_property([](const Vec3& v) -> float { return v.z(); } ); + vec3Type[sol::meta_function::to_string] = [](const Vec3& v) { std::stringstream ss; ss << "(" << v.x() << ", " << v.y() << ", " << v.z() << ")"; return ss.str(); }; - vec3Type[sol::meta_function::unary_minus] = [](const osg::Vec3f& a) { return -a; }; - vec3Type[sol::meta_function::addition] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a + b; }; - vec3Type[sol::meta_function::subtraction] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a - b; }; - vec3Type[sol::meta_function::equal_to] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a == b; }; + vec3Type[sol::meta_function::unary_minus] = [](const Vec3& a) { return -a; }; + vec3Type[sol::meta_function::addition] = [](const Vec3& a, const Vec3& b) { return a + b; }; + vec3Type[sol::meta_function::subtraction] = [](const Vec3& a, const Vec3& b) { return a - b; }; + vec3Type[sol::meta_function::equal_to] = [](const Vec3& a, const Vec3& b) { return a == b; }; vec3Type[sol::meta_function::multiplication] = sol::overload( - [](const osg::Vec3f& a, float c) { return a * c; }, - [](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; }); - vec3Type[sol::meta_function::division] = [](const osg::Vec3f& a, float c) { return a / c; }; - vec3Type[sol::meta_function::involution] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; }; - vec3Type["dot"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; }; - vec3Type["cross"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; }; - vec3Type["length"] = &osg::Vec3f::length; - vec3Type["length2"] = &osg::Vec3f::length2; - vec3Type["normalize"] = [](const osg::Vec3f& v) { + [](const Vec3& a, float c) { return a * c; }, + [](const Vec3& a, const Vec3& b) { return a * b; }); + vec3Type[sol::meta_function::division] = [](const Vec3& a, float c) { return a / c; }; + vec3Type[sol::meta_function::involution] = [](const Vec3& a, const Vec3& b) { return a ^ b; }; + vec3Type["dot"] = [](const Vec3& a, const Vec3& b) { return a * b; }; + vec3Type["cross"] = [](const Vec3& a, const Vec3& b) { return a ^ b; }; + vec3Type["length"] = &Vec3::length; + vec3Type["length2"] = &Vec3::length2; + vec3Type["normalize"] = [](const Vec3& v) { float len = v.length(); if (len == 0) - return std::make_tuple(osg::Vec3f(), 0.f); + return std::make_tuple(Vec3(), 0.f); else return std::make_tuple(v * (1.f / len), len); }; + // Lua bindings for Transform + sol::usertype transMType = lua.new_usertype("TransformM"); + sol::usertype transQType = lua.new_usertype("TransformQ"); + sol::table transforms(lua, sol::create); + util["transform"] = LuaUtil::makeReadOnly(transforms); + + transforms["identity"] = sol::make_object(lua, TransformM{osg::Matrixf::identity()}); + transforms["move"] = sol::overload( + [](const Vec3& v) { return TransformM{osg::Matrixf::translate(v)}; }, + [](float x, float y, float z) { return TransformM{osg::Matrixf::translate(x, y, z)}; }); + transforms["scale"] = sol::overload( + [](const Vec3& v) { return TransformM{osg::Matrixf::scale(v)}; }, + [](float x, float y, float z) { return TransformM{osg::Matrixf::scale(x, y, z)}; }); + transforms["rotate"] = [](float angle, const Vec3& axis) { return TransformQ{osg::Quat(angle, axis)}; }; + transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(1, 0, 0))}; }; + transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 1, 0))}; }; + transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, 1))}; }; + + transMType[sol::meta_function::multiplication] = sol::overload( + [](const TransformM& a, const Vec3& b) { return a.mM.preMult(b); }, + [](const TransformM& a, const TransformM& b) { return TransformM{b.mM * a.mM}; }, + [](const TransformM& a, const TransformQ& b) + { + TransformM res{a.mM}; + res.mM.preMultRotate(b.mQ); + return res; + }); + transMType[sol::meta_function::to_string] = [](const TransformM& m) + { + osg::Vec3f trans, scale; + osg::Quat rotation, so; + m.mM.decompose(trans, rotation, scale, so); + osg::Quat::value_type rot_angle, so_angle; + osg::Vec3f rot_axis, so_axis; + rotation.getRotate(rot_angle, rot_axis); + so.getRotate(so_angle, so_axis); + std::stringstream ss; + ss << "TransformM{ "; + if (trans.length2() > 0) + ss << "move(" << trans.x() << ", " << trans.y() << ", " << trans.z() << ") "; + if (rot_angle != 0) + ss << "rotation(angle=" << rot_angle << ", axis=(" + << rot_axis.x() << ", " << rot_axis.y() << ", " << rot_axis.z() << ")) "; + if (scale.x() != 1 || scale.y() != 1 || scale.z() != 1) + ss << "scale(" << scale.x() << ", " << scale.y() << ", " << scale.z() << ") "; + if (so_angle != 0) + ss << "rotation(angle=" << so_angle << ", axis=(" + << so_axis.x() << ", " << so_axis.y() << ", " << so_axis.z() << ")) "; + ss << "}"; + return ss.str(); + }; + transMType["inverse"] = [](const TransformM& m) + { + TransformM res; + if (!res.mM.invert_4x3(m.mM)) + throw std::runtime_error("This Transform is not invertible"); + return res; + }; + + transQType[sol::meta_function::multiplication] = sol::overload( + [](const TransformQ& a, const Vec3& b) { return a.mQ * b; }, + [](const TransformQ& a, const TransformQ& b) { return TransformQ{b.mQ * a.mQ}; }, + [](const TransformQ& a, const TransformM& b) + { + TransformM res{b}; + res.mM.postMultRotate(a.mQ); + return res; + }); + transQType[sol::meta_function::to_string] = [](const TransformQ& q) + { + osg::Quat::value_type angle; + osg::Vec3f axis; + q.mQ.getRotate(angle, axis); + std::stringstream ss; + ss << "TransformQ{ rotation(angle=" << angle << ", axis=(" + << axis.x() << ", " << axis.y() << ", " << axis.z() << ")) }"; + return ss.str(); + }; + transQType["inverse"] = [](const TransformQ& q) { return TransformQ{q.mQ.inverse()}; }; + // Utility functions util["clamp"] = [](float value, float from, float to) { return std::clamp(value, from, to); }; // NOTE: `util["clamp"] = std::clamp` causes error 'AddressSanitizer: stack-use-after-scope' diff --git a/components/lua/utilpackage.hpp b/components/lua/utilpackage.hpp index 9e73723561..d26bfdb027 100644 --- a/components/lua/utilpackage.hpp +++ b/components/lua/utilpackage.hpp @@ -1,11 +1,24 @@ #ifndef COMPONENTS_LUA_UTILPACKAGE_H #define COMPONENTS_LUA_UTILPACKAGE_H -#include // missing from sol/sol.hpp +#include +#include +#include + #include namespace LuaUtil { + using Vec2 = osg::Vec2f; + using Vec3 = osg::Vec3f; + + // For performance reasons "Transform" is implemented as 2 types with the same interface. + // Transform supports only composition, inversion, and applying to a 3d vector. + struct TransformM { osg::Matrixf mM; }; + struct TransformQ { osg::Quat mQ; }; + + inline TransformM asTransform(const osg::Matrixf& m) { return {m}; } + inline TransformQ asTransform(const osg::Quat& q) { return {q}; } sol::table initUtilPackage(sol::state&); diff --git a/components/misc/barrier.hpp b/components/misc/barrier.hpp index b3fe944b04..277e40814a 100644 --- a/components/misc/barrier.hpp +++ b/components/misc/barrier.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_BARRIER_H #define OPENMW_BARRIER_H +#include #include #include @@ -12,7 +13,9 @@ namespace Misc public: /// @param count number of threads to wait on explicit Barrier(int count) : mThreadCount(count), mRendezvousCount(0), mGeneration(0) - {} + { + assert(count >= 0); + } /// @brief stop execution of threads until count distinct threads reach this point /// @param func callable to be executed once after all threads have met @@ -22,8 +25,8 @@ namespace Misc std::unique_lock lock(mMutex); ++mRendezvousCount; - const int currentGeneration = mGeneration; - if (mRendezvousCount == mThreadCount) + const unsigned int currentGeneration = mGeneration; + if (mRendezvousCount == mThreadCount || mThreadCount == 0) { ++mGeneration; mRendezvousCount = 0; @@ -37,9 +40,9 @@ namespace Misc } private: - int mThreadCount; - int mRendezvousCount; - int mGeneration; + unsigned int mThreadCount; + unsigned int mRendezvousCount; + unsigned int mGeneration; mutable std::mutex mMutex; std::condition_variable mRendezvous; }; diff --git a/components/misc/pathhelpers.hpp b/components/misc/pathhelpers.hpp new file mode 100644 index 0000000000..88913a1f7b --- /dev/null +++ b/components/misc/pathhelpers.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHHELPERS_H +#define OPENMW_COMPONENTS_MISC_PATHHELPERS_H + +#include + +namespace Misc +{ + inline std::string_view getFileExtension(std::string_view file) + { + if (auto extPos = file.find_last_of('.'); extPos != std::string::npos) + { + file.remove_prefix(extPos + 1); + return file; + } + return {}; + } +} + +#endif diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index abc170c02e..3061b329c2 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -15,6 +15,7 @@ #include #include +#include #include @@ -51,7 +52,7 @@ class Drawable : public osg::Drawable { public: // Stage 0: update widget animations and controllers. Run during the Update traversal. - class FrameUpdate : public osg::Drawable::UpdateCallback + class FrameUpdate : public SceneUtil::NodeCallback { public: FrameUpdate() @@ -64,10 +65,9 @@ public: mRenderManager = renderManager; } - void update(osg::NodeVisitor*, osg::Drawable*) override + void operator()(osg::Node*, osg::NodeVisitor*) { - if (mRenderManager) - mRenderManager->update(); + mRenderManager->update(); } private: @@ -75,7 +75,7 @@ public: }; // Stage 1: collect draw calls. Run during the Cull traversal. - class CollectDrawCalls : public osg::Drawable::CullCallback + class CollectDrawCalls : public SceneUtil::NodeCallback { public: CollectDrawCalls() @@ -88,13 +88,9 @@ public: mRenderManager = renderManager; } - bool cull(osg::NodeVisitor*, osg::Drawable*, osg::State*) const override + void operator()(osg::Node*, osg::NodeVisitor*) { - if (!mRenderManager) - return false; - mRenderManager->collectDrawCalls(); - return false; } private: diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index d48c55ad7d..ddded82156 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -72,6 +72,7 @@ KeyframeController::KeyframeController() KeyframeController::KeyframeController(const KeyframeController ©, const osg::CopyOp ©op) : SceneUtil::KeyframeController(copy, copyop) + , SceneUtil::NodeCallback(copy, copyop) , mRotations(copy.mRotations) , mXRotations(copy.mXRotations) , mYRotations(copy.mYRotations) @@ -133,16 +134,15 @@ osg::Vec3f KeyframeController::getTranslation(float time) const return osg::Vec3f(); } -void KeyframeController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void KeyframeController::operator() (NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) { if (hasInput()) { - NifOsg::MatrixTransform* trans = static_cast(node); - osg::Matrix mat = trans->getMatrix(); + osg::Matrix mat = node->getMatrix(); float time = getInputValue(nv); - Nif::Matrix3& rot = trans->mRotationScale; + Nif::Matrix3& rot = node->mRotationScale; bool setRot = false; if(!mRotations.empty()) @@ -168,7 +168,7 @@ void KeyframeController::operator() (osg::Node* node, osg::NodeVisitor* nv) for (int j=0;j<3;++j) rot.mValues[i][j] = mat(j,i); // NB column/row major difference - float& scale = trans->mScale; + float& scale = node->mScale; if(!mScales.empty()) scale = mScales.interpKey(time); @@ -179,7 +179,7 @@ void KeyframeController::operator() (osg::Node* node, osg::NodeVisitor* nv) if(!mTranslations.empty()) mat.setTrans(mTranslations.interpKey(time)); - trans->setMatrix(mat); + node->setMatrix(mat); } traverse(node, nv); @@ -190,8 +190,8 @@ GeomMorpherController::GeomMorpherController() } GeomMorpherController::GeomMorpherController(const GeomMorpherController ©, const osg::CopyOp ©op) - : osg::Drawable::UpdateCallback(copy, copyop) - , Controller(copy) + : Controller(copy) + , SceneUtil::NodeCallback(copy, copyop) , mKeyFrames(copy.mKeyFrames) { } @@ -217,9 +217,8 @@ GeomMorpherController::GeomMorpherController(const Nif::NiGeomMorpherController* } } -void GeomMorpherController::update(osg::NodeVisitor *nv, osg::Drawable *drawable) +void GeomMorpherController::operator()(SceneUtil::MorphGeometry* node, osg::NodeVisitor *nv) { - SceneUtil::MorphGeometry* morphGeom = static_cast(drawable); if (hasInput()) { if (mKeyFrames.size() <= 1) @@ -232,11 +231,11 @@ void GeomMorpherController::update(osg::NodeVisitor *nv, osg::Drawable *drawable if (!(*it).empty()) val = it->interpKey(input); - SceneUtil::MorphGeometry::MorphTarget& target = morphGeom->getMorphTarget(i); + SceneUtil::MorphGeometry::MorphTarget& target = node->getMorphTarget(i); if (target.getWeight() != val) { target.setWeight(val); - morphGeom->dirty(); + node->dirty(); } } } @@ -311,7 +310,7 @@ VisController::VisController() } VisController::VisController(const VisController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mData(copy.mData) , mMask(copy.mMask) @@ -352,14 +351,14 @@ RollController::RollController(const Nif::NiFloatInterpolator* interpolator) } RollController::RollController(const RollController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mData(copy.mData) , mStartingTime(copy.mStartingTime) { } -void RollController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void RollController::operator() (osg::MatrixTransform* node, osg::NodeVisitor* nv) { traverse(node, nv); @@ -370,15 +369,14 @@ void RollController::operator() (osg::Node* node, osg::NodeVisitor* nv) mStartingTime = newTime; float value = mData.interpKey(getInputValue(nv)); - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); + osg::Matrix matrix = node->getMatrix(); // Rotate around "roll" axis. // Note: in original game rotation speed is the framerate-dependent in a very tricky way. // Do not replicate this behaviour until we will really need it. // For now consider controller's current value as an angular speed in radians per 1/60 seconds. matrix = osg::Matrix::rotate(value * duration * 60.f, 0, 0, 1) * matrix; - transform->setMatrix(matrix); + node->setMatrix(matrix); } } @@ -544,29 +542,28 @@ ParticleSystemController::ParticleSystemController() } ParticleSystemController::ParticleSystemController(const ParticleSystemController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mEmitStart(copy.mEmitStart) , mEmitStop(copy.mEmitStop) { } -void ParticleSystemController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void ParticleSystemController::operator() (osgParticle::ParticleProcessor* node, osg::NodeVisitor* nv) { - osgParticle::ParticleProcessor* emitter = static_cast(node); if (hasInput()) { float time = getInputValue(nv); - emitter->getParticleSystem()->setFrozen(false); - emitter->setEnabled(time >= mEmitStart && time < mEmitStop); + node->getParticleSystem()->setFrozen(false); + node->setEnabled(time >= mEmitStart && time < mEmitStop); } else - emitter->getParticleSystem()->setFrozen(true); + node->getParticleSystem()->setFrozen(true); traverse(node, nv); } PathController::PathController(const PathController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mPath(copy.mPath) , mPercent(copy.mPercent) @@ -591,7 +588,7 @@ float PathController::getPercent(float time) const return percent; } -void PathController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void PathController::operator() (osg::MatrixTransform* node, osg::NodeVisitor* nv) { if (mPath.empty() || mPercent.empty() || !hasInput()) { @@ -599,14 +596,13 @@ void PathController::operator() (osg::Node* node, osg::NodeVisitor* nv) return; } - osg::MatrixTransform* trans = static_cast(node); - osg::Matrix mat = trans->getMatrix(); + osg::Matrix mat = node->getMatrix(); float time = getInputValue(nv); float percent = getPercent(time); osg::Vec3f pos(mPath.interpKey(percent)); mat.setTrans(pos); - trans->setMatrix(mat); + node->setMatrix(mat); traverse(node, nv); } diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index b459166931..c6311fd5fc 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -14,19 +15,27 @@ #include -#include -#include -#include - - namespace osg { class Material; + class MatrixTransform; +} + +namespace osgParticle +{ + class ParticleProcessor; +} + +namespace SceneUtil +{ + class MorphGeometry; } namespace NifOsg { + class MatrixTransform; + // interpolation of keyframes template class ValueInterpolator @@ -207,8 +216,7 @@ namespace NifOsg float getMaximum() const override; }; - /// Must be set on a SceneUtil::MorphGeometry. - class GeomMorpherController : public osg::Drawable::UpdateCallback, public SceneUtil::Controller + class GeomMorpherController : public SceneUtil::Controller, public SceneUtil::NodeCallback { public: GeomMorpherController(const Nif::NiGeomMorpherController* ctrl); @@ -217,13 +225,13 @@ namespace NifOsg META_Object(NifOsg, GeomMorpherController) - void update(osg::NodeVisitor* nv, osg::Drawable* drawable) override; + void operator()(SceneUtil::MorphGeometry*, osg::NodeVisitor*); private: std::vector mKeyFrames; }; - class KeyframeController : public SceneUtil::KeyframeController + class KeyframeController : public SceneUtil::KeyframeController, public SceneUtil::NodeCallback { public: // This is used if there's no interpolator but there is data (Morrowind meshes). @@ -241,7 +249,7 @@ namespace NifOsg osg::Vec3f getTranslation(float time) const override; - void operator() (osg::Node*, osg::NodeVisitor*) override; + void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); private: QuaternionInterpolator mRotations; @@ -276,7 +284,7 @@ namespace NifOsg std::set mTextureUnits; }; - class VisController : public osg::NodeCallback, public SceneUtil::Controller + class VisController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: std::vector mData; @@ -291,10 +299,10 @@ namespace NifOsg META_Object(NifOsg, VisController) - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osg::Node* node, osg::NodeVisitor* nv); }; - class RollController : public osg::NodeCallback, public SceneUtil::Controller + class RollController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: FloatInterpolator mData; @@ -306,7 +314,7 @@ namespace NifOsg RollController() = default; RollController(const RollController& copy, const osg::CopyOp& copyop); - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osg::MatrixTransform* node, osg::NodeVisitor* nv); META_Object(NifOsg, RollController) }; @@ -377,7 +385,7 @@ namespace NifOsg void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; }; - class ParticleSystemController : public osg::NodeCallback, public SceneUtil::Controller + class ParticleSystemController : public SceneUtil::NodeCallback, public SceneUtil::Controller { public: ParticleSystemController(const Nif::NiParticleSystemController* ctrl); @@ -386,14 +394,14 @@ namespace NifOsg META_Object(NifOsg, ParticleSystemController) - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osgParticle::ParticleProcessor* node, osg::NodeVisitor* nv); private: float mEmitStart; float mEmitStop; }; - class PathController : public osg::NodeCallback, public SceneUtil::Controller + class PathController : public SceneUtil::NodeCallback, public SceneUtil::Controller { public: PathController(const Nif::NiPathController* ctrl); @@ -402,7 +410,7 @@ namespace NifOsg META_Object(NifOsg, PathController) - void operator() (osg::Node*, osg::NodeVisitor*) override; + void operator() (osg::MatrixTransform*, osg::NodeVisitor*); private: Vec3Interpolator mPath; diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 0432aef5b4..838895eb47 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -109,24 +109,21 @@ namespace // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale // set just like a regular MatrixTransform, but the rotation set will be overridden in order to face the camera. - // Must be set as a cull callback. - class BillboardCallback : public osg::NodeCallback + class BillboardCallback : public SceneUtil::NodeCallback { public: BillboardCallback() { } BillboardCallback(const BillboardCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) { } META_Object(NifOsg, BillboardCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::Matrix modelView = *cv->getModelViewMatrix(); // attempt to preserve scale @@ -143,7 +140,7 @@ namespace cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } @@ -1134,8 +1131,6 @@ namespace NifOsg trans->addChild(toAttach); parentNode->addChild(trans); } - // create partsys stateset in order to pass in ShaderVisitor like all other Drawables - partsys->getOrCreateStateSet(); } void handleNiGeometryData(osg::Geometry *geometry, const Nif::NiGeometryData* data, const std::vector& boundTextures, const std::string& name) @@ -1923,8 +1918,6 @@ namespace NifOsg void applyDrawableProperties(osg::Node* node, const std::vector& properties, SceneUtil::CompositeStateSetUpdater* composite, bool hasVertexColors, int animflags) { - osg::StateSet* stateset = node->getOrCreateStateSet(); - // Specular lighting is enabled by default, but there's a quirk... bool specEnabled = true; osg::ref_ptr mat (new osg::Material); @@ -2006,15 +1999,15 @@ namespace NifOsg if (blendFunc->getDestination() == GL_DST_ALPHA) blendFunc->setDestination(GL_ONE); blendFunc = shareAttribute(blendFunc); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); bool noSort = (alphaprop->flags>>13)&1; if (!noSort) - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + node->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); else - stateset->setRenderBinToInherit(); + node->getOrCreateStateSet()->setRenderBinToInherit(); } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); stateset->removeMode(GL_BLEND); @@ -2025,9 +2018,9 @@ namespace NifOsg { osg::ref_ptr alphaFunc (new osg::AlphaFunc(getTestMode((alphaprop->flags>>10)&0x7), alphaprop->data.threshold/255.f)); alphaFunc = shareAttribute(alphaFunc); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); stateset->removeMode(GL_ALPHA_TEST); @@ -2083,8 +2076,10 @@ namespace NifOsg mat = shareAttribute(mat); + osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); + if (emissiveMult != 1.f) + stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); } }; diff --git a/components/nifosg/particle.cpp b/components/nifosg/particle.cpp index e2e1f92cf3..7821b9c2b8 100644 --- a/components/nifosg/particle.cpp +++ b/components/nifosg/particle.cpp @@ -68,20 +68,16 @@ void ParticleSystem::drawImplementation(osg::RenderInfo& renderInfo) const osgParticle::ParticleSystem::drawImplementation(renderInfo); } -void InverseWorldMatrix::operator()(osg::Node *node, osg::NodeVisitor *nv) +void InverseWorldMatrix::operator()(osg::MatrixTransform *node, osg::NodeVisitor *nv) { - if (nv && nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) - { - osg::NodePath path = nv->getNodePath(); - path.pop_back(); + osg::NodePath path = nv->getNodePath(); + path.pop_back(); - osg::MatrixTransform* trans = static_cast(node); + osg::Matrix mat = osg::computeLocalToWorld( path ); + mat.orthoNormalize(mat); // don't undo the scale + mat.invert(mat); + node->setMatrix(mat); - osg::Matrix mat = osg::computeLocalToWorld( path ); - mat.orthoNormalize(mat); // don't undo the scale - mat.invert(mat); - trans->setMatrix(mat); - } traverse(node,nv); } diff --git a/components/nifosg/particle.hpp b/components/nifosg/particle.hpp index b0a46f0ca5..8b724545f4 100644 --- a/components/nifosg/particle.hpp +++ b/components/nifosg/particle.hpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include "controller.hpp" // ValueInterpolator @@ -57,20 +57,20 @@ namespace NifOsg // Node callback used to set the inverse of the parent's world matrix on the MatrixTransform // that the callback is attached to. Used for certain particle systems, // so that the particles do not move with the node they are attached to. - class InverseWorldMatrix : public osg::NodeCallback + class InverseWorldMatrix : public SceneUtil::NodeCallback { public: InverseWorldMatrix() { } - InverseWorldMatrix(const InverseWorldMatrix& copy, const osg::CopyOp& op) - : osg::Object(), osg::NodeCallback() + InverseWorldMatrix(const InverseWorldMatrix& copy, const osg::CopyOp& copyop) + : osg::Object(copy, copyop), SceneUtil::NodeCallback(copy, copyop) { } META_Object(NifOsg, InverseWorldMatrix) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); }; class ParticleShooter : public osgParticle::Shooter diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index b1b0f74176..0d0f81962b 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -121,8 +122,7 @@ BulletShapeManager::~BulletShapeManager() osg::ref_ptr BulletShapeManager::getShape(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr shape; osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); @@ -130,12 +130,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & shape = osg::ref_ptr(static_cast(obj.get())); else { - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); - - if (ext == "nif") + if (Misc::getFileExtension(normalized) == "nif") { NifBullet::BulletNifLoader loader; shape = loader.load(*mNifFileManager->get(normalized)); @@ -180,8 +175,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & osg::ref_ptr BulletShapeManager::cacheInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr instance = createInstance(normalized); if (instance) @@ -191,8 +185,7 @@ osg::ref_ptr BulletShapeManager::cacheInstance(const std::s osg::ref_ptr BulletShapeManager::getInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); if (obj.get()) diff --git a/components/resource/imagemanager.cpp b/components/resource/imagemanager.cpp index 37e76359ff..a544b7b621 100644 --- a/components/resource/imagemanager.cpp +++ b/components/resource/imagemanager.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "objectcache.hpp" @@ -83,8 +84,7 @@ namespace Resource osg::ref_ptr ImageManager::getImage(const std::string &filename) { - std::string normalized = filename; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(filename); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -103,10 +103,7 @@ namespace Resource return mWarningImage; } - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); + const std::string ext(Misc::getFileExtension(normalized)); osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); if (!reader) { diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 444d2bd7aa..8aa32a28bc 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include "animation.hpp" @@ -133,8 +135,7 @@ namespace Resource osg::ref_ptr KeyframeManager::get(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -142,8 +143,7 @@ namespace Resource else { osg::ref_ptr loaded (new SceneUtil::KeyframeHolder); - std::string ext = Resource::getFileExtension(normalized); - if (ext == "kf") + if (Misc::getFileExtension(normalized) == "kf") { NifOsg::Loader::loadKf(Nif::NIFFilePtr(new Nif::NIFFile(mVFS->getNormalized(normalized), normalized)), *loaded.get()); } diff --git a/components/resource/resourcemanager.hpp b/components/resource/resourcemanager.hpp index ccb065e3bf..b2b71f4635 100644 --- a/components/resource/resourcemanager.hpp +++ b/components/resource/resourcemanager.hpp @@ -59,6 +59,7 @@ namespace Resource /// How long to keep objects in cache after no longer being referenced. void setExpiryDelay (double expiryDelay) override { mExpiryDelay = expiryDelay; } + float getExpiryDelay() const { return mExpiryDelay; } const VFS::Manager* getVFS() const { return mVFS; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 33a9ab99a1..c5ef957c3e 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -17,6 +18,7 @@ #include #include +#include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include #include @@ -33,21 +36,18 @@ #include "imagemanager.hpp" #include "niffilemanager.hpp" #include "objectcache.hpp" -#include "multiobjectcache.hpp" namespace { - class InitWorldSpaceParticlesCallback : public osg::NodeCallback + class InitWorldSpaceParticlesCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osgParticle::ParticleSystem* node, osg::NodeVisitor* nv) { - osgParticle::ParticleSystem* partsys = static_cast(node); - // HACK: Ignore the InverseWorldMatrix transform the particle system is attached to - if (partsys->getNumParents() && partsys->getParent(0)->getNumParents()) - transformInitialParticles(partsys, partsys->getParent(0)->getParent(0)); + if (node->getNumParents() && node->getParent(0)->getNumParents()) + transformInitialParticles(node, node->getParent(0)->getParent(0)); node->removeUpdateCallback(this); } @@ -216,7 +216,88 @@ namespace Resource int mMaxAnisotropy; }; + // Check Collada extra descriptions + class ColladaAlphaTrickVisitor : public osg::NodeVisitor + { + public: + ColladaAlphaTrickVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + { + } + osg::AlphaFunc::ComparisonFunction getTestMode(std::string mode) + { + if (mode == "ALWAYS") return osg::AlphaFunc::ALWAYS; + if (mode == "LESS") return osg::AlphaFunc::LESS; + if (mode == "EQUAL") return osg::AlphaFunc::EQUAL; + if (mode == "LEQUAL") return osg::AlphaFunc::LEQUAL; + if (mode == "GREATER") return osg::AlphaFunc::GREATER; + if (mode == "NOTEQUAL") return osg::AlphaFunc::NOTEQUAL; + if (mode == "GEQUAL") return osg::AlphaFunc::GEQUAL; + if (mode == "NEVER") return osg::AlphaFunc::NEVER; + + Log(Debug::Warning) << "Unexpected alpha testing mode: " << mode; + return osg::AlphaFunc::LEQUAL; + } + + void apply(osg::Node& node) override + { + if (osg::StateSet* stateset = node.getStateSet()) + { + if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN) + { + osg::ref_ptr depth = SceneUtil::createDepth(); + depth->setWriteMask(false); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + else if (stateset->getRenderingHint() == osg::StateSet::OPAQUE_BIN) + { + osg::ref_ptr depth = SceneUtil::createDepth(); + depth->setWriteMask(true); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + } + + /* Check if the has + correct format for OpenMW: alphatest mode value MaterialName + e.g alphatest GEQUAL 0.8 MyAlphaTestedMaterial */ + std::vector descriptions = node.getDescriptions(); + for (auto description : descriptions) + { + mDescriptions.emplace_back(description); + } + + // Iterate each description, and see if the current node uses the specified material for alpha testing + if (node.getStateSet()) + { + for (auto description : mDescriptions) + { + std::vector descriptionParts; + std::istringstream descriptionStringStream(description); + for (std::string part; std::getline(descriptionStringStream, part, ' ');) + { + descriptionParts.emplace_back(part); + } + + if (descriptionParts.size() > (3) && descriptionParts.at(3) == node.getStateSet()->getName()) + { + if (descriptionParts.at(0) == "alphatest") + { + osg::AlphaFunc::ComparisonFunction mode = getTestMode(descriptionParts.at(1)); + osg::ref_ptr alphaFunc (new osg::AlphaFunc(mode, std::stod(descriptionParts.at(2)))); + node.getStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + } + } + } + } + + traverse(node); + } + private: + std::vector mDescriptions; + }; SceneManager::SceneManager(const VFS::Manager *vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) : ResourceManager(vfs) @@ -229,7 +310,6 @@ namespace Resource , mLightingMethod(SceneUtil::LightingMethod::FFP) , mConvertAlphaTestToAlphaToCoverage(false) , mDepthFormat(0) - , mInstanceCache(new MultiObjectCache) , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) , mNifFileManager(nifFileManager) @@ -251,10 +331,11 @@ namespace Resource return mForceShaders; } - void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix, bool translucentFramebuffer, bool forceShadersForNode) + void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix, bool forceShadersForNode, const osg::Program* programTemplate) { - osg::ref_ptr shaderVisitor(createShaderVisitor(shaderPrefix, translucentFramebuffer)); + osg::ref_ptr shaderVisitor(createShaderVisitor(shaderPrefix)); shaderVisitor->setAllowedToModifyStateSets(false); + shaderVisitor->setProgramTemplate(programTemplate); if (forceShadersForNode) shaderVisitor->setForceShaders(true); node->accept(*shaderVisitor); @@ -329,6 +410,13 @@ namespace Resource void SceneManager::setLightingMethod(SceneUtil::LightingMethod method) { mLightingMethod = method; + + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + { + osg::ref_ptr program = new osg::Program; + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); + mShaderManager->setProgramTemplate(program); + } } SceneUtil::LightingMethod SceneManager::getLightingMethod() const @@ -358,10 +446,7 @@ namespace Resource bool SceneManager::checkLoaded(const std::string &name, double timeStamp) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - return mCache->checkInObjectCache(normalized, timeStamp); + return mCache->checkInObjectCache(mVFS->normalizeFilename(name), timeStamp); } /// @brief Callback to read image files from the VFS. @@ -391,12 +476,12 @@ namespace Resource osg::ref_ptr load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) { - std::string ext = Resource::getFileExtension(normalizedFilename); + auto ext = Misc::getFileExtension(normalizedFilename); if (ext == "nif") return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); else { - osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); + osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext)); if (!reader) { std::stringstream errormsg; @@ -426,6 +511,18 @@ namespace Resource if (nameFinder.mFoundNode) nameFinder.mFoundNode->setNodeMask(hiddenNodeMask); + if (ext == "dae") + { + // Collada alpha testing + Resource::ColladaAlphaTrickVisitor colladaAlphaTrickVisitor; + result.getNode()->accept(colladaAlphaTrickVisitor); + + result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); + result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false)); + } + + return result.getNode(); } } @@ -533,8 +630,7 @@ namespace Resource osg::ref_ptr SceneManager::getTemplate(const std::string &name, bool compile) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -574,22 +670,18 @@ namespace Resource osg::ref_ptr shaderVisitor (createShaderVisitor()); loaded->accept(*shaderVisitor); - // share state - // do this before optimizing so the optimizer will be able to combine nodes more aggressively - // note, because StateSets will be shared at this point, StateSets can not be modified inside the optimizer - mSharedStateMutex.lock(); - mSharedStateManager->share(loaded.get()); - mSharedStateMutex.unlock(); - if (canOptimize(normalized)) { SceneUtil::Optimizer optimizer; + optimizer.setSharedStateManager(mSharedStateManager, &mSharedStateMutex); optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); - static const unsigned int options = getOptimizationOptions(); + static const unsigned int options = getOptimizationOptions()|SceneUtil::Optimizer::SHARE_DUPLICATE_STATE; optimizer.optimize(loaded, options); } + else + shareState(loaded); if (compile && mIncrementalCompileOperation) mIncrementalCompileOperation->add(loaded); @@ -601,22 +693,6 @@ namespace Resource } } - osg::ref_ptr SceneManager::cacheInstance(const std::string &name) - { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr node = createInstance(normalized); - - // Note: osg::clone() does not calculate bound volumes. - // Do it immediately, otherwise we will need to update them for all objects - // during first update traversal, what may lead to stuttering during cell transitions - node->getBound(); - - mInstanceCache->addEntryToObjectCache(normalized, node.get()); - return node; - } - osg::ref_ptr SceneManager::createInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); @@ -642,15 +718,7 @@ namespace Resource osg::ref_ptr SceneManager::getInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); - if (obj.get()) - return static_cast(obj.get()); - - return createInstance(normalized); - + return createInstance(name); } osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) @@ -668,7 +736,6 @@ namespace Resource void SceneManager::releaseGLObjects(osg::State *state) { mCache->releaseGLObjects(state); - mInstanceCache->releaseGLObjects(state); mShaderManager->releaseGLObjects(state); @@ -756,8 +823,6 @@ namespace Resource { ResourceManager::updateCache(referenceTime); - mInstanceCache->removeUnreferencedObjectsInCache(); - mSharedStateMutex.lock(); mSharedStateManager->prune(); mSharedStateMutex.unlock(); @@ -787,7 +852,6 @@ namespace Resource std::lock_guard lock(mSharedStateMutex); mSharedStateManager->clearCache(); - mInstanceCache->clear(); } void SceneManager::reportStats(unsigned int frameNumber, osg::Stats *stats) const @@ -805,10 +869,9 @@ namespace Resource } stats->setAttribute(frameNumber, "Node", mCache->getCacheSize()); - stats->setAttribute(frameNumber, "Node Instance", mInstanceCache->getCacheSize()); } - Shader::ShaderVisitor *SceneManager::createShaderVisitor(const std::string& shaderPrefix, bool translucentFramebuffer) + Shader::ShaderVisitor *SceneManager::createShaderVisitor(const std::string& shaderPrefix) { Shader::ShaderVisitor* shaderVisitor = new Shader::ShaderVisitor(*mShaderManager.get(), *mImageManager, shaderPrefix); shaderVisitor->setForceShaders(mForceShaders); @@ -819,15 +882,6 @@ namespace Resource shaderVisitor->setSpecularMapPattern(mSpecularMapPattern); shaderVisitor->setApplyLightingToEnvMaps(mApplyLightingToEnvMaps); shaderVisitor->setConvertAlphaTestToAlphaToCoverage(mConvertAlphaTestToAlphaToCoverage); - shaderVisitor->setTranslucentFramebuffer(translucentFramebuffer); return shaderVisitor; } - - std::string getFileExtension(const std::string& file) - { - size_t extPos = file.find_last_of('.'); - if (extPos != std::string::npos && extPos+1 < file.size()) - return file.substr(extPos+1); - return std::string(); - } } diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index 9d798177a1..b5d3e453a0 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -65,8 +65,6 @@ namespace Resource std::vector> mObjects; }; - class MultiObjectCache; - /// @brief Handles loading and caching of scenes, e.g. .nif files or .osg files /// @note Some methods of the scene manager can be used from any thread, see the methods documentation for more details. class SceneManager : public ResourceManager @@ -78,7 +76,7 @@ namespace Resource Shader::ShaderManager& getShaderManager(); /// Re-create shaders for this node, need to call this if alpha testing, texture stages or vertex color mode have changed. - void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false, bool forceShadersForNode = false); + void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects", bool forceShadersForNode = false, const osg::Program* programTemplate = nullptr); /// Applying shaders to a node may replace some fixed-function state. /// This restores it. @@ -113,6 +111,11 @@ namespace Resource void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + enum class UBOBinding + { + // If we add more UBO's, we should probably assign their bindings dynamically according to the current count of UBO's in the programTemplate + LightBuffer + }; void setLightingMethod(SceneUtil::LightingMethod method); SceneUtil::LightingMethod getLightingMethod() const; @@ -129,12 +132,6 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - /// Create an instance of the given scene template and cache it for later use, so that future calls to getInstance() can simply - /// return this cached object instead of creating a new one. - /// @note The returned ref_ptr may be kept around by the caller to ensure that the object stays in cache for as long as needed. - /// @note Thread safe. - osg::ref_ptr cacheInstance(const std::string& name); - osg::ref_ptr createInstance(const std::string& name); osg::ref_ptr createInstance(const osg::Node* base); @@ -191,7 +188,7 @@ namespace Resource private: - Shader::ShaderVisitor* createShaderVisitor(const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false); + Shader::ShaderVisitor* createShaderVisitor(const std::string& shaderPrefix = "objects"); std::unique_ptr mShaderManager; bool mForceShaders; @@ -207,8 +204,6 @@ namespace Resource bool mConvertAlphaTestToAlphaToCoverage; GLenum mDepthFormat; - osg::ref_ptr mInstanceCache; - osg::ref_ptr mSharedStateManager; mutable std::mutex mSharedStateMutex; diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index 9881d0458b..b3705f69cc 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -376,7 +376,6 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "Texture", "StateSet", "Node", - "Node Instance", "Shape", "Shape Instance", "Image", diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp index e09541cb9a..5be6924a09 100644 --- a/components/sceneutil/keyframe.hpp +++ b/components/sceneutil/keyframe.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include #include @@ -11,20 +11,18 @@ namespace SceneUtil { - class KeyframeController : public osg::NodeCallback, public SceneUtil::Controller + class KeyframeController : public SceneUtil::Controller, public virtual osg::Callback { public: KeyframeController() {} KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) + : osg::Callback(copy, copyop) , SceneUtil::Controller(copy) {} META_Object(SceneUtil, KeyframeController) virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } - - virtual void operator() (osg::Node* node, osg::NodeVisitor* nodeVisitor) override { traverse(node, nodeVisitor); } }; /// Wrapper object containing an animation track as a ref-countable osg::Object. diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index cc320aecf8..13e367baa4 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -26,7 +26,7 @@ namespace SceneUtil mType = type; } - void LightController::operator ()(osg::Node* node, osg::NodeVisitor* nv) + void LightController::operator ()(SceneUtil::LightSource* node, osg::NodeVisitor* nv) { double time = nv->getFrameStamp()->getSimulationTime(); if (mStartTime == 0) @@ -38,7 +38,7 @@ namespace SceneUtil if (mType == LT_Normal) { - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor); + node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor); traverse(node, nv); return; } @@ -62,8 +62,7 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - auto* lightSource = static_cast(node); - lightSource->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * lightSource->getActorFade()); + node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * node->getActorFade()); traverse(node, nv); } diff --git a/components/sceneutil/lightcontroller.hpp b/components/sceneutil/lightcontroller.hpp index 36b2e868e5..b67cd59472 100644 --- a/components/sceneutil/lightcontroller.hpp +++ b/components/sceneutil/lightcontroller.hpp @@ -1,15 +1,16 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_LIGHTCONTROLLER_H #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTCONTROLLER_H -#include +#include #include namespace SceneUtil { + class LightSource; + /// @brief Controller class to handle a pulsing and/or flickering light - /// @note Must be set on a SceneUtil::LightSource. - class LightController : public osg::NodeCallback + class LightController : public SceneUtil::NodeCallback { public: enum LightType { @@ -26,7 +27,7 @@ namespace SceneUtil void setDiffuse(const osg::Vec4f& color); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(SceneUtil::LightSource* node, osg::NodeVisitor* nv); private: LightType mType; diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 63a475566b..f38fd80d26 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -294,9 +295,9 @@ namespace SceneUtil osg::ref_ptr ubo = new osg::UniformBufferObject; buffer->getData()->setBufferObject(ubo); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); #else - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); #endif stateset->setAttributeAndModes(ubb, mode); @@ -604,19 +605,19 @@ namespace SceneUtil // Set on a LightSource. Adds the light source to its light manager for the current frame. // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager. - class CollectLightCallback : public osg::NodeCallback + class CollectLightCallback : public SceneUtil::NodeCallback { public: CollectLightCallback() : mLightManager(nullptr) { } CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , mLightManager(nullptr) { } META_Object(SceneUtil, SceneUtil::CollectLightCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (!mLightManager) { @@ -636,19 +637,11 @@ namespace SceneUtil }; // Set on a LightManager. Clears the data from the previous frame. - class LightManagerUpdateCallback : public osg::NodeCallback + class LightManagerUpdateCallback : public SceneUtil::NodeCallback { public: - LightManagerUpdateCallback() - { } - LightManagerUpdateCallback(const LightManagerUpdateCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) - { } - - META_Object(SceneUtil, LightManagerUpdateCallback) - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { LightManager* lightManager = static_cast(node); lightManager->update(nv->getTraversalNumber()); @@ -657,49 +650,47 @@ namespace SceneUtil } }; - class LightManagerCullCallback : public osg::NodeCallback + class LightManagerCullCallback : public SceneUtil::NodeCallback { public: - LightManagerCullCallback(LightManager* lightManager) : mLightManager(lightManager), mLastFrameNumber(0) {} + LightManagerCullCallback() : mLastFrameNumber(0) {} - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(LightManager* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - if (mLastFrameNumber != cv->getTraversalNumber()) { mLastFrameNumber = cv->getTraversalNumber(); - if (mLightManager->getLightingMethod() == LightingMethod::SingleUBO) + if (node->getLightingMethod() == LightingMethod::SingleUBO) { - auto stateset = mLightManager->getStateSet(); - auto bo = mLightManager->getLightBuffer(mLastFrameNumber); + auto stateset = node->getStateSet(); + auto bo = node->getLightBuffer(mLastFrameNumber); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); #else - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); #endif stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); } - auto sun = mLightManager->getSunlight(); + auto sun = node->getSunlight(); if (sun) { // we must defer uploading the transformation to view-space position to deal with different cameras (e.g. reflection RTT). - if (mLightManager->getLightingMethod() == LightingMethod::PerObjectUniform) + if (node->getLightingMethod() == LightingMethod::PerObjectUniform) { osg::Matrixf lightMat; configurePosition(lightMat, sun->getPosition()); configureAmbient(lightMat, sun->getAmbient()); configureDiffuse(lightMat, sun->getDiffuse()); configureSpecular(lightMat, sun->getSpecular()); - mLightManager->setSunlightBuffer(lightMat, mLastFrameNumber); + node->setSunlightBuffer(lightMat, mLastFrameNumber); } else { - auto buf = mLightManager->getLightBuffer(mLastFrameNumber); + auto buf = node->getLightBuffer(mLastFrameNumber); buf->setCachedSunPos(sun->getPosition()); buf->setAmbient(0, sun->getAmbient()); @@ -709,11 +700,10 @@ namespace SceneUtil } } - traverse(node, nv); + traverse(node, cv); } private: - LightManager* mLightManager; size_t mLastFrameNumber; }; @@ -736,7 +726,7 @@ namespace SceneUtil // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably // available, regardless of extensions, until GLSL 140. mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); - mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); } LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) @@ -751,7 +741,7 @@ namespace SceneUtil void initSharedLayout(osg::GLExtensions* ext, int handle) const { - constexpr std::array index = { static_cast(Shader::UBOBinding::LightBuffer) }; + constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; int totalBlockSize = -1; int stride = -1; @@ -906,7 +896,7 @@ namespace SceneUtil getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); - addCullCallback(new LightManagerCullCallback(this)); + addCullCallback(new LightManagerCullCallback()); } LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) @@ -1202,9 +1192,10 @@ namespace SceneUtil return stateset; } - const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum) + const std::vector& LightManager::getLightsInViewSpace(osgUtil::CullVisitor *cv, const osg::RefMatrix* viewMatrix, size_t frameNum) { - bool isReflection = isReflectionCamera(camera); + osg::Camera* camera = cv->getCurrentCamera(); + osg::observer_ptr camPtr (camera); auto it = mLightsInViewSpace.find(camPtr); @@ -1212,13 +1203,15 @@ namespace SceneUtil { it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; + bool isReflection = isReflectionCamera(camera); + for (const auto& transform : mLights) { osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); float radius = transform.mLightSource->getRadius(); - osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius * mPointLightRadiusMultiplier); + osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius); transformBoundingSphere(worldViewMat, viewBound); if (!isReflection && mPointLightFadeEnd != 0.f) @@ -1232,6 +1225,15 @@ namespace SceneUtil light->setDiffuse(light->getDiffuse() * fade); } + // remove lights culled by this camera + if (!usingFFP()) + { + viewBound._radius *= 2.f; + if (cv->getModelViewCullingStack().front().isCulled(viewBound)) + continue; + viewBound._radius /= 2.f; + } + viewBound._radius *= mPointLightRadiusMultiplier; LightSourceViewBound l; l.mLightSource = transform.mLightSource; l.mViewBound = viewBound; @@ -1283,12 +1285,10 @@ namespace SceneUtil mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } - void LightListCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) + void LightListCallback::operator()(osg::Node *node, osgUtil::CullVisitor *cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - bool pushedState = pushLightState(node, cv); - traverse(node, nv); + traverse(node, cv); if (pushedState) cv->popStateSet(); } @@ -1306,46 +1306,41 @@ namespace SceneUtil return false; // Possible optimizations: - // - cull list of lights by the camera frustum // - organize lights in a quad tree - // update light list if necessary - // makes sure we don't update it more than once per frame when rendering with multiple cameras - if (mLastFrameNumber != cv->getTraversalNumber()) + mLastFrameNumber = cv->getTraversalNumber(); + + // Don't use Camera::getViewMatrix, that one might be relative to another camera! + const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); + const std::vector& lights = mLightManager->getLightsInViewSpace(cv, viewMatrix, mLastFrameNumber); + + // get the node bounds in view space + // NB do not node->getBound() * modelView, that would apply the node's transformation twice + osg::BoundingSphere nodeBound; + osg::Transform* transform = node->asTransform(); + if (transform) { - mLastFrameNumber = cv->getTraversalNumber(); - - // Don't use Camera::getViewMatrix, that one might be relative to another camera! - const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); - const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix, mLastFrameNumber); - - // get the node bounds in view space - // NB do not node->getBound() * modelView, that would apply the node's transformation twice - osg::BoundingSphere nodeBound; - osg::Transform* transform = node->asTransform(); - if (transform) - { - for (size_t i = 0; i < transform->getNumChildren(); ++i) - nodeBound.expandBy(transform->getChild(i)->getBound()); - } - else - nodeBound = node->getBound(); - osg::Matrixf mat = *cv->getModelViewMatrix(); - transformBoundingSphere(mat, nodeBound); - - mLightList.clear(); - for (size_t i = 0; i < lights.size(); ++i) - { - const LightManager::LightSourceViewBound& l = lights[i]; - - if (mIgnoredLightSources.count(l.mLightSource)) - continue; - - if (l.mViewBound.intersects(nodeBound)) - mLightList.push_back(&l); - } + for (size_t i = 0; i < transform->getNumChildren(); ++i) + nodeBound.expandBy(transform->getChild(i)->getBound()); } + else + nodeBound = node->getBound(); + osg::Matrixf mat = *cv->getModelViewMatrix(); + transformBoundingSphere(mat, nodeBound); + + mLightList.clear(); + for (size_t i = 0; i < lights.size(); ++i) + { + const LightManager::LightSourceViewBound& l = lights[i]; + + if (mIgnoredLightSources.count(l.mLightSource)) + continue; + + if (l.mViewBound.intersects(nodeBound)) + mLightList.push_back(&l); + } + if (!mLightList.empty()) { size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); @@ -1354,31 +1349,25 @@ namespace SceneUtil if (mLightList.size() > maxLights) { - // remove lights culled by this camera LightManager::LightList lightList = mLightList; - for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) - { - osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); - osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius * 2.0; - osg::CullingSet& cullingSet = stack.front(); - if (cullingSet.isCulled(bs)) + if (mLightManager->usingFFP()) + { + for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) { - it = lightList.erase(it); - continue; + osg::BoundingSphere bs = (*it)->mViewBound; + bs._radius = bs._radius * 2.0; + if (cv->getModelViewCullingStack().front().isCulled(bs)) + it = lightList.erase(it); + else + ++it; } - else - ++it; } - if (lightList.size() > maxLights) - { - // sort by proximity to camera, then get rid of furthest away lights - std::sort(lightList.begin(), lightList.end(), sortLights); - while (lightList.size() > maxLights) - lightList.pop_back(); - } + // sort by proximity to camera, then get rid of furthest away lights + std::sort(lightList.begin(), lightList.end(), sortLights); + while (lightList.size() > maxLights) + lightList.pop_back(); stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); } else diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index 6dbe7a3f75..b518a4723c 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -16,6 +16,7 @@ #include #include +#include namespace osgUtil { @@ -156,7 +157,7 @@ namespace SceneUtil /// Internal use only, called automatically by the LightSource's UpdateCallback void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum); - const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix, size_t frameNum); + const std::vector& getLightsInViewSpace(osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum); osg::ref_ptr getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix); @@ -254,7 +255,7 @@ namespace SceneUtil /// starting point is to attach a LightListCallback to each game object's base node. /// @note Not thread safe for CullThreadPerCamera threading mode. /// @note Due to lack of OSG support, the callback does not work on Drawables. - class LightListCallback : public osg::NodeCallback + class LightListCallback : public SceneUtil::NodeCallback { public: LightListCallback() @@ -262,7 +263,7 @@ namespace SceneUtil , mLastFrameNumber(0) {} LightListCallback(const LightListCallback& copy, const osg::CopyOp& copyop) - : osg::Object(copy, copyop), osg::NodeCallback(copy, copyop) + : osg::Object(copy, copyop), SceneUtil::NodeCallback(copy, copyop) , mLightManager(copy.mLightManager) , mLastFrameNumber(0) , mIgnoredLightSources(copy.mIgnoredLightSources) @@ -270,7 +271,7 @@ namespace SceneUtil META_Object(SceneUtil, LightListCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osgUtil::CullVisitor* nv); bool pushLightState(osg::Node* node, osgUtil::CullVisitor* nv); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 95ff8f2b01..db7aef7cb1 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -353,18 +353,20 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) } // namespace -MWShadowTechnique::ComputeLightSpaceBounds::ComputeLightSpaceBounds(osg::Viewport* viewport, const osg::Matrixd& projectionMatrix, osg::Matrixd& viewMatrix) : +MWShadowTechnique::ComputeLightSpaceBounds::ComputeLightSpaceBounds() : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN) { setCullingMode(osg::CullSettings::VIEW_FRUSTUM_CULLING); - pushViewport(viewport); - pushProjectionMatrix(new osg::RefMatrix(projectionMatrix)); - pushModelViewMatrix(new osg::RefMatrix(viewMatrix), osg::Transform::ABSOLUTE_RF); - setName("SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds,AcceptedByComponentsTerrainQuadTreeWorld"); } +void MWShadowTechnique::ComputeLightSpaceBounds::reset() +{ + osg::CullStack::reset(); + _bb = osg::BoundingBox(); +} + void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Node& node) { if (isCulled(node)) return; @@ -421,9 +423,9 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Transform& transform // absolute transforms won't affect a shadow map so their subgraphs should be ignored. if (transform.getReferenceFrame() == osg::Transform::RELATIVE_RF) { - osg::ref_ptr matrix = new osg::RefMatrix(*getModelViewMatrix()); + osg::RefMatrix* matrix = createOrReuseMatrix(*getModelViewMatrix()); transform.computeLocalToWorldMatrix(*matrix, this); - pushModelViewMatrix(matrix.get(), transform.getReferenceFrame()); + pushModelViewMatrix(matrix, transform.getReferenceFrame()); traverse(transform); @@ -1125,7 +1127,12 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // osg::ElapsedTime timer; osg::ref_ptr viewport = new osg::Viewport(0,0,2048,2048); - ComputeLightSpaceBounds clsb(viewport.get(), projectionMatrix, viewMatrix); + if (!_clsb) _clsb = new ComputeLightSpaceBounds; + ComputeLightSpaceBounds& clsb = *_clsb; + clsb.reset(); + clsb.pushViewport(viewport); + clsb.pushProjectionMatrix(new osg::RefMatrix(projectionMatrix)); + clsb.pushModelViewMatrix(new osg::RefMatrix(viewMatrix), osg::Transform::ABSOLUTE_RF); clsb.setTraversalMask(_shadowedScene->getCastsShadowTraversalMask()); osg::Matrixd invertModelView; @@ -1139,6 +1146,12 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) _shadowedScene->accept(clsb); + clsb.popCullingSet(); + + clsb.popModelViewMatrix(); + clsb.popProjectionMatrix(); + clsb.popViewport(); + // OSG_NOTICE<<"Extents of LightSpace "<getNumChildren()==1 && transform->getChild(0)->asTransform()!=0 && transform->getChild(0)->asTransform()->asMatrixTransform()!=0 && - transform->getChild(0)->asTransform()->getDataVariance()==osg::Object::STATIC) + (!transform->getChild(0)->getStateSet() || transform->getChild(0)->getStateSet()->referenceCount()==1) && + transform->getChild(0)->getDataVariance()==osg::Object::STATIC) { // now combine with its child. osg::MatrixTransform* child = transform->getChild(0)->asTransform()->asMatrixTransform(); @@ -903,11 +913,11 @@ void Optimizer::RemoveRedundantNodesVisitor::removeRedundantNodes() unsigned int childIndex = (*pitr)->getChildIndex(group); for (unsigned int i=0; igetNumChildren(); ++i) { - osg::Node* child = group->getChild(i); - (*pitr)->insertChild(childIndex++, child); + if (i==0) + (*pitr)->setChild(childIndex, group->getChild(i)); + else + (*pitr)->insertChild(childIndex+i, group->getChild(i)); } - - (*pitr)->removeChild(group); } group->removeChildren(0, group->getNumChildren()); diff --git a/components/sceneutil/optimizer.hpp b/components/sceneutil/optimizer.hpp index 2d6293e231..7b32bafee2 100644 --- a/components/sceneutil/optimizer.hpp +++ b/components/sceneutil/optimizer.hpp @@ -25,6 +25,12 @@ //#include #include +#include + +namespace osgDB +{ + class SharedStateManager; +} //namespace osgUtil { namespace SceneUtil { @@ -65,7 +71,7 @@ class Optimizer public: - Optimizer() : _mergeAlphaBlending(false) {} + Optimizer() : _mergeAlphaBlending(false), _sharedStateManager(nullptr), _sharedStateMutex(nullptr) {} virtual ~Optimizer() {} enum OptimizationOptions @@ -121,6 +127,8 @@ class Optimizer void setMergeAlphaBlending(bool merge) { _mergeAlphaBlending = merge; } void setViewPoint(const osg::Vec3f& viewPoint) { _viewPoint = viewPoint; } + void setSharedStateManager(osgDB::SharedStateManager* sharedStateManager, std::mutex* sharedStateMutex) { _sharedStateMutex = sharedStateMutex; _sharedStateManager = sharedStateManager; } + /** Reset internal data to initial state - the getPermissibleOptionsMap is cleared.*/ void reset(); @@ -258,6 +266,9 @@ class Optimizer osg::Vec3f _viewPoint; bool _mergeAlphaBlending; + osgDB::SharedStateManager* _sharedStateManager; + mutable std::mutex* _sharedStateMutex; + public: /** Flatten Static Transform nodes by applying their transform to the diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp index 02c9558ab3..520b2d177b 100644 --- a/components/sceneutil/osgacontroller.cpp +++ b/components/sceneutil/osgacontroller.cpp @@ -1,19 +1,12 @@ #include -#include #include #include #include -#include #include -#include #include -#include -#include #include -#include -#include #include #include @@ -55,19 +48,6 @@ namespace SceneUtil } } - void LinkVisitor::handle_stateset(osg::StateSet* stateset) - { - if (!stateset) - return; - const osg::StateSet::AttributeList& attributeList = stateset->getAttributeList(); - for (auto attribute : attributeList) - { - osg::StateAttribute* sattr = attribute.second.first.get(); - osgAnimation::UpdateMatrixTransform* umt = dynamic_cast(sattr->getUpdateCallback()); //Can this even be in sa? - if (umt) link(umt); - } - } - void LinkVisitor::setAnimation(Resource::Animation* animation) { mAnimation = animation; @@ -75,10 +55,6 @@ namespace SceneUtil void LinkVisitor::apply(osg::Node& node) { - osg::StateSet* st = node.getStateSet(); - if (st) - handle_stateset(st); - osg::Callback* cb = node.getUpdateCallback(); while (cb) { @@ -88,21 +64,11 @@ namespace SceneUtil cb = cb->getNestedCallback(); } - traverse( node ); + if (node.getNumChildrenRequiringUpdateTraversal()) + traverse( node ); } - void LinkVisitor::apply(osg::Geode& node) - { - for (unsigned int i = 0; i < node.getNumDrawables(); i++) - { - osg::Drawable* drawable = node.getDrawable(i); - if (drawable && drawable->getStateSet()) - handle_stateset(drawable->getStateSet()); - } - apply(static_cast(node)); - } - - OsgAnimationController::OsgAnimationController(const OsgAnimationController ©, const osg::CopyOp ©op) : SceneUtil::KeyframeController(copy, copyop) + OsgAnimationController::OsgAnimationController(const OsgAnimationController ©, const osg::CopyOp ©op) : SceneUtil::KeyframeController(copy, copyop), SceneUtil::NodeCallback(copy, copyop) , mEmulatedAnimations(copy.mEmulatedAnimations) { mLinker = nullptr; diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp index 46538fa813..26212a3b99 100644 --- a/components/sceneutil/osgacontroller.hpp +++ b/components/sceneutil/osgacontroller.hpp @@ -3,16 +3,11 @@ #include #include -#include #include -#include -#include -#include -#include -#include #include #include +#include #include #include @@ -32,19 +27,15 @@ namespace SceneUtil virtual void link(osgAnimation::UpdateMatrixTransform* umt); - virtual void handle_stateset(osg::StateSet* stateset); - virtual void setAnimation(Resource::Animation* animation); virtual void apply(osg::Node& node) override; - virtual void apply(osg::Geode& node) override; - protected: Resource::Animation* mAnimation; }; - class OsgAnimationController : public SceneUtil::KeyframeController + class OsgAnimationController : public SceneUtil::KeyframeController, public SceneUtil::NodeCallback { public: /// @brief Handles the animation for osgAnimation formats @@ -61,7 +52,7 @@ namespace SceneUtil void update(float time, const std::string& animationName); /// @brief Called every frame for osgAnimation - void operator() (osg::Node*, osg::NodeVisitor*) override; + void operator() (osg::Node*, osg::NodeVisitor*); /// @brief Sets details of the animations void setEmulatedAnimations(const std::vector& emulatedAnimations); diff --git a/components/sceneutil/recastmesh.cpp b/components/sceneutil/recastmesh.cpp index 8d6d47c2b6..97efe010df 100644 --- a/components/sceneutil/recastmesh.cpp +++ b/components/sceneutil/recastmesh.cpp @@ -52,10 +52,10 @@ namespace SceneUtil for (const Heightfield& heightfield : recastMesh.getHeightfields()) { - const Mesh mesh = makeMesh(heightfield); + const Mesh heightfieldMesh = makeMesh(heightfield); const int indexShift = static_cast(vertices.size() / 3); - std::copy(mesh.getVertices().begin(), mesh.getVertices().end(), std::back_inserter(vertices)); - std::transform(mesh.getIndices().begin(), mesh.getIndices().end(), std::back_inserter(indices), + std::copy(heightfieldMesh.getVertices().begin(), heightfieldMesh.getVertices().end(), std::back_inserter(vertices)); + std::transform(heightfieldMesh.getIndices().begin(), heightfieldMesh.getIndices().end(), std::back_inserter(indices), [&] (int index) { return index + indexShift; }); } diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp index a661a90279..3b60c80afd 100644 --- a/components/sceneutil/rtt.cpp +++ b/components/sceneutil/rtt.cpp @@ -6,23 +6,19 @@ #include #include +#include #include namespace SceneUtil { - // RTTNode's cull callback - class CullCallback : public osg::NodeCallback + class CullCallback : public SceneUtil::NodeCallback { public: - CullCallback(RTTNode* group) - : mGroup(group) {} - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(RTTNode* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - mGroup->cull(cv); + node->cull(cv); } - RTTNode* mGroup; }; RTTNode::RTTNode(uint32_t textureWidth, uint32_t textureHeight, int renderOrderNum, bool doPerViewMapping) @@ -31,7 +27,7 @@ namespace SceneUtil , mRenderOrderNum(renderOrderNum) , mDoPerViewMapping(doPerViewMapping) { - addCullCallback(new CullCallback(this)); + addCullCallback(new CullCallback); setCullingActive(false); } diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp index 14dbc14178..af62b581c9 100644 --- a/components/sceneutil/shadowsbin.cpp +++ b/components/sceneutil/shadowsbin.cpp @@ -54,6 +54,7 @@ ShadowsBin::ShadowsBin() mShaderAlphaTestStateSet = new osg::StateSet; mShaderAlphaTestStateSet->addUniform(new osg::Uniform("alphaTestShadows", true)); + mShaderAlphaTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); for (size_t i = 0; i < sCastingPrograms.size(); ++i) diff --git a/components/sceneutil/statesetupdater.cpp b/components/sceneutil/statesetupdater.cpp index a8232f938e..9ef82b9271 100644 --- a/components/sceneutil/statesetupdater.cpp +++ b/components/sceneutil/statesetupdater.cpp @@ -68,7 +68,7 @@ namespace SceneUtil } StateSetUpdater::StateSetUpdater(const StateSetUpdater ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) { } diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 263f76ae54..35be9cb434 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_STATESETCONTROLLER_H #define OPENMW_COMPONENTS_SCENEUTIL_STATESETCONTROLLER_H -#include +#include #include #include @@ -28,7 +28,7 @@ namespace SceneUtil /// @note When used as a CullCallback, StateSetUpdater will have no effect on leaf nodes such as osg::Geometry and must be used on branch nodes only. /// @note Do not add the same StateSetUpdater to multiple nodes. /// @note Do not add multiple StateSetUpdaters on the same Node as they will conflict - instead use the CompositeStateSetUpdater. - class StateSetUpdater : public osg::NodeCallback + class StateSetUpdater : public SceneUtil::NodeCallback { public: StateSetUpdater(); @@ -36,7 +36,7 @@ namespace SceneUtil META_Object(SceneUtil, StateSetUpdater) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osg::NodeVisitor* nv); /// Apply state - to override in derived classes /// @note Due to the double buffering approach you *have* to apply all state @@ -46,8 +46,7 @@ namespace SceneUtil /// Set default state - optionally override in derived classes /// @par May be used e.g. to allocate StateAttributes. virtual void setDefaults(osg::StateSet* stateset) {} - - protected: + /// Reset mStateSets, forcing a setDefaults() on the next frame. Can be used to change the defaults if needed. void reset(); diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index 007a6637a8..5c2f8c2934 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -11,11 +11,15 @@ #include #include #include +#include +#include +#include #include #include #include #include +#include #ifndef GL_DEPTH32F_STENCIL8_NV #define GL_DEPTH32F_STENCIL8_NV 0x8DAC @@ -171,8 +175,7 @@ void GlowUpdater::setDuration(float duration) } // Allows camera to render to a color and floating point depth texture with a multisampled framebuffer. -// Must be set on a camera's cull callback. -class AttachMultisampledDepthColorCallback : public osg::NodeCallback +class AttachMultisampledDepthColorCallback : public SceneUtil::NodeCallback { public: AttachMultisampledDepthColorCallback(osg::Texture2D* colorTex, osg::Texture2D* depthTex, int samples, int colorSamples) @@ -192,14 +195,14 @@ public: mFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(depthTex)); } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::RenderStage* renderStage = static_cast(nv)->getCurrentRenderStage(); + osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); renderStage->setMultisampleResolveFramebufferObject(mFbo); renderStage->setFrameBufferObject(mMsaaFbo); - traverse(node, nv); + traverse(node, cv); } private: diff --git a/components/sceneutil/util.hpp b/components/sceneutil/util.hpp index 0bca32c325..4d19714d66 100644 --- a/components/sceneutil/util.hpp +++ b/components/sceneutil/util.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 33f79415f1..e057cfac02 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -17,7 +16,6 @@ namespace Shader { ShaderManager::ShaderManager() - : mLightingMethod(SceneUtil::LightingMethod::FFP) { } @@ -26,11 +24,6 @@ namespace Shader mPath = path; } - void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method) - { - mLightingMethod = method; - } - bool addLineDirectivesAfterConditionalBlocks(std::string& source) { for (size_t position = 0; position < source.length(); ) @@ -345,19 +338,16 @@ namespace Shader return shaderIt->second; } - osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader) + osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate) { std::lock_guard lock(mMutex); ProgramMap::iterator found = mPrograms.find(std::make_pair(vertexShader, fragmentShader)); if (found == mPrograms.end()) { - osg::ref_ptr program (new osg::Program); + if (!programTemplate) programTemplate = mProgramTemplate; + osg::ref_ptr program = programTemplate ? static_cast(programTemplate->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; program->addShader(vertexShader); program->addShader(fragmentShader); - program->addBindAttribLocation("aOffset", 6); - program->addBindAttribLocation("aRotation", 7); - if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) - program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 2450f0d6dc..d0ee069b10 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -8,29 +8,11 @@ #include #include - -#include - -#include - -namespace Resource -{ - class SceneManager; -} - -namespace SceneUtil -{ - enum class LightingMethod; -} +#include namespace Shader { - enum class UBOBinding - { - LightBuffer - }; - /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's. /// @par Shader templates can get the value of a define with the syntax @define. class ShaderManager @@ -41,8 +23,6 @@ namespace Shader void setShaderPath(const std::string& path); - void setLightingMethod(SceneUtil::LightingMethod method); - typedef std::map DefineMap; /// Create or retrieve a shader instance. @@ -53,7 +33,10 @@ namespace Shader /// @note Thread safe. osg::ref_ptr getShader(const std::string& templateName, const DefineMap& defines, osg::Shader::Type shaderType); - osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader); + osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate=nullptr); + + const osg::Program* getProgramTemplate() const { return mProgramTemplate; } + void setProgramTemplate(const osg::Program* program) { mProgramTemplate = program; } /// Get (a copy of) the DefineMap used to construct all shaders DefineMap getGlobalDefines(); @@ -81,9 +64,9 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; - SceneUtil::LightingMethod mLightingMethod; - std::mutex mMutex; + + osg::ref_ptr mProgramTemplate; }; bool parseFors(std::string& source, const std::string& templateName); diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 26511d4654..6709ee842e 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -1,5 +1,8 @@ #include "shadervisitor.hpp" +#include +#include + #include #include #include @@ -108,12 +111,10 @@ namespace Shader , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) , mConvertAlphaTestToAlphaToCoverage(false) - , mTranslucentFramebuffer(false) , mShaderManager(shaderManager) , mImageManager(imageManager) , mDefaultShaderPrefix(defaultShaderPrefix) { - mRequirements.emplace_back(); } void ShaderVisitor::setForceShaders(bool force) @@ -264,7 +265,7 @@ namespace Shader mRequirements.back().mShaderRequired = true; } } - else if (!mTranslucentFramebuffer) + else Log(Debug::Error) << "ShaderVisitor encountered unknown texture " << texture; } } @@ -340,15 +341,6 @@ namespace Shader mRequirements.back().mShaderRequired = true; } } - - if (diffuseMap) - { - if (!writableStateSet) - writableStateSet = getWritableStateSet(node); - // We probably shouldn't construct a new version of this each time as Uniforms use pointer comparison for early-out. - // Also it should probably belong to the shader manager or be applied by the shadows bin - writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); - } } const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); @@ -427,7 +419,10 @@ namespace Shader void ShaderVisitor::pushRequirements(osg::Node& node) { - mRequirements.push_back(mRequirements.back()); + if (mRequirements.empty()) + mRequirements.emplace_back(); + else + mRequirements.push_back(mRequirements.back()); mRequirements.back().mNode = &node; } @@ -467,6 +462,9 @@ namespace Shader defineMap[texIt->second + std::string("UV")] = std::to_string(texIt->first); } + if (defineMap["diffuseMap"] == "0") + writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); + defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); @@ -548,8 +546,6 @@ namespace Shader updateAddedState(*writableUserData, addedState); } - defineMap["translucentFramebuffer"] = mTranslucentFramebuffer ? "1" : "0"; - std::string shaderPrefix; if (!node.getUserValue("shaderPrefix", shaderPrefix)) shaderPrefix = mDefaultShaderPrefix; @@ -559,7 +555,7 @@ namespace Shader if (vertexShader && fragmentShader) { - auto program = mShaderManager.getProgram(vertexShader, fragmentShader); + auto program = mShaderManager.getProgram(vertexShader, fragmentShader, mProgramTemplate); writableStateSet->setAttributeAndModes(program, osg::StateAttribute::ON); addedState->setAttributeAndModes(program); @@ -765,11 +761,6 @@ namespace Shader mConvertAlphaTestToAlphaToCoverage = convert; } - void ShaderVisitor::setTranslucentFramebuffer(bool translucent) - { - mTranslucentFramebuffer = translucent; - } - ReinstateRemovedStateVisitor::ReinstateRemovedStateVisitor(bool allowedToModifyStateSets) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAllowedToModifyStateSets(allowedToModifyStateSets) diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index a5add473a6..5f9739ea90 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_SHADERVISITOR_H #include +#include namespace Resource { @@ -19,6 +20,8 @@ namespace Shader public: ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string& defaultShaderPrefix); + void setProgramTemplate(const osg::Program* programTemplate) { mProgramTemplate = programTemplate; } + /// By default, only bump mapped objects will have a shader added to them. /// Setting force = true will cause all objects to render using shaders, regardless of having a bump map. void setForceShaders(bool force); @@ -42,8 +45,6 @@ namespace Shader void setConvertAlphaTestToAlphaToCoverage(bool convert); - void setTranslucentFramebuffer(bool translucent); - void apply(osg::Node& node) override; void apply(osg::Drawable& drawable) override; @@ -69,8 +70,6 @@ namespace Shader bool mConvertAlphaTestToAlphaToCoverage; - bool mTranslucentFramebuffer; - ShaderManager& mShaderManager; Resource::ImageManager& mImageManager; @@ -109,6 +108,8 @@ namespace Shader void createProgram(const ShaderRequirements& reqs); void ensureFFP(osg::Node& node); bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs); + + osg::ref_ptr mProgramTemplate; }; class ReinstateRemovedStateVisitor : public osg::NodeVisitor diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index c34ed9ab0a..927530660e 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -3,12 +3,12 @@ #include #include #include -#include #include "world.hpp" #include "../esm/loadland.hpp" #include +#include namespace Terrain { @@ -21,13 +21,14 @@ CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask, { } -void CellBorder::createCellBorderGeometry(int x, int y) +osg::ref_ptr CellBorder::createBorderGeometry(float x, float y, float size, Terrain::Storage* terrain, Resource::SceneManager* sceneManager, int mask, + float offset, osg::Vec4f color) { const int cellSize = ESM::Land::REAL_SIZE; const int borderSegments = 40; - const float offset = 10.0; osg::Vec3 cellCorner = osg::Vec3(x * cellSize,y * cellSize,0); + size *= cellSize; osg::ref_ptr vertices = new osg::Vec3Array; osg::ref_ptr colors = new osg::Vec4Array; @@ -35,22 +36,22 @@ void CellBorder::createCellBorderGeometry(int x, int y) normals->push_back(osg::Vec3(0.0f,-1.0f, 0.0f)); - float borderStep = cellSize / ((float) borderSegments); + float borderStep = size / ((float)borderSegments); for (int i = 0; i <= 2 * borderSegments; ++i) { osg::Vec3f pos = i < borderSegments ? osg::Vec3(i * borderStep,0.0f,0.0f) : - osg::Vec3(cellSize,(i - borderSegments) * borderStep,0.0f); + osg::Vec3(size, (i - borderSegments) * borderStep,0.0f); pos += cellCorner; - pos += osg::Vec3f(0,0,mWorld->getHeightAt(pos) + offset); + pos += osg::Vec3f(0,0, terrain->getHeightAt(pos) + offset); vertices->push_back(pos); osg::Vec4f col = i % 2 == 0 ? osg::Vec4f(0,0,0,1) : - osg::Vec4f(1,1,0,1); + color; colors->push_back(col); } @@ -64,8 +65,8 @@ void CellBorder::createCellBorderGeometry(int x, int y) border->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP,0,vertices->size())); - osg::ref_ptr borderGeode = new osg::Geode; - borderGeode->addDrawable(border.get()); + osg::ref_ptr borderGeode = new osg::Group; + borderGeode->addChild(border.get()); osg::StateSet *stateSet = borderGeode->getOrCreateStateSet(); osg::ref_ptr material (new osg::Material); @@ -76,10 +77,15 @@ void CellBorder::createCellBorderGeometry(int x, int y) polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); - mSceneManager->recreateShaders(borderGeode, "debug"); + sceneManager->recreateShaders(borderGeode, "debug"); + borderGeode->setNodeMask(mask); - borderGeode->setNodeMask(mBorderMask); + return borderGeode; +} +void CellBorder::createCellBorderGeometry(int x, int y) +{ + auto borderGeode = createBorderGeometry(x, y, 1.f, mWorld->getStorage(), mSceneManager, mBorderMask); mRoot->addChild(borderGeode); mCellBorderNodes[std::make_pair(x,y)] = borderGeode; diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index ee8fd72593..4481816a55 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -11,6 +11,7 @@ namespace Resource namespace Terrain { + class Storage; class World; /** @@ -31,6 +32,8 @@ namespace Terrain */ void destroyCellBorderGeometry(); + static osg::ref_ptr createBorderGeometry(float x, float y, float size, Storage* terrain, Resource::SceneManager* sceneManager, int mask, float offset = 10.0, osg::Vec4f color = { 1,1,0,0 }); + protected: Terrain::World *mWorld; Resource::SceneManager* mSceneManager; diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index 8809a75bb9..e040cbdd93 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -40,6 +39,17 @@ ChunkManager::ChunkManager(Storage *storage, Resource::SceneManager *sceneMgr, T mMultiPassRoot->setAttributeAndModes(material, osg::StateAttribute::ON); } +struct FindChunkTemplate +{ + void operator() (ChunkId id, osg::Object* obj) + { + if (std::get<0>(id) == std::get<0>(mId) && std::get<1>(id) == std::get<1>(mId)) + mFoundTemplate = obj; + } + ChunkId mId; + osg::ref_ptr mFoundTemplate; +}; + osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { ChunkId id = std::make_tuple(center, lod, lodFlags); @@ -48,7 +58,11 @@ osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f& cen return static_cast(obj.get()); else { - osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile); + FindChunkTemplate find; + find.mId = id; + mCache->call(find); + TerrainDrawable* templateGeometry = find.mFoundTemplate ? static_cast(find.mFoundTemplate.get()) : nullptr; + osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile, templateGeometry); mCache->addEntryToObjectCache(id, node.get()); return node; } @@ -166,24 +180,45 @@ std::vector > ChunkManager::createPasses(float chunk return ::Terrain::createPasses(useShaders, &mSceneManager->getShaderManager(), layers, blendmapTextures, blendmapScale, blendmapScale); } -osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile) +osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry) { - osg::ref_ptr positions (new osg::Vec3Array); - osg::ref_ptr normals (new osg::Vec3Array); - osg::ref_ptr colors (new osg::Vec4ubArray); - colors->setNormalize(true); - - osg::ref_ptr vbo (new osg::VertexBufferObject); - positions->setVertexBufferObject(vbo); - normals->setVertexBufferObject(vbo); - colors->setVertexBufferObject(vbo); - - mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); - osg::ref_ptr geometry (new TerrainDrawable); - geometry->setVertexArray(positions); - geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); - geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + + if (!templateGeometry) + { + osg::ref_ptr positions (new osg::Vec3Array); + osg::ref_ptr normals (new osg::Vec3Array); + osg::ref_ptr colors (new osg::Vec4ubArray); + colors->setNormalize(true); + + mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + else + { + // Unfortunately we need to copy vertex data because of poor coupling with VertexBufferObject. + osg::ref_ptr positions = static_cast(templateGeometry->getVertexArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr normals = static_cast(templateGeometry->getNormalArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr colors = static_cast(templateGeometry->getColorArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + geometry->setUseDisplayList(false); geometry->setUseVertexBufferObjects(true); @@ -203,32 +238,44 @@ osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Ve geometry->setStateSet(mMultiPassRoot); - if (useCompositeMap) + if (templateGeometry) { - osg::ref_ptr compositeMap = new CompositeMap; - compositeMap->mTexture = createCompositeMapRTT(); - - createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); - - mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); - - geometry->setCompositeMap(compositeMap); - geometry->setCompositeMapRenderer(mCompositeMapRenderer); - - TextureLayer layer; - layer.mDiffuseMap = compositeMap->mTexture; - layer.mParallax = false; - layer.mSpecular = false; - geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), &mSceneManager->getShaderManager(), std::vector(1, layer), std::vector >(), 1.f, 1.f)); + if (templateGeometry->getCompositeMap()) + { + geometry->setCompositeMap(templateGeometry->getCompositeMap()); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); + } + geometry->setPasses(templateGeometry->getPasses()); } else { - geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + if (useCompositeMap) + { + osg::ref_ptr compositeMap = new CompositeMap; + compositeMap->mTexture = createCompositeMapRTT(); + + createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); + + mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); + + geometry->setCompositeMap(compositeMap); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); + + TextureLayer layer; + layer.mDiffuseMap = compositeMap->mTexture; + layer.mParallax = false; + layer.mSpecular = false; + geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), &mSceneManager->getShaderManager(), std::vector(1, layer), std::vector >(), 1.f, 1.f)); + } + else + { + geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + } } geometry->setupWaterBoundingBox(-1, chunkSize * mStorage->getCellWorldSize() / numVerts); - if (compile && mSceneManager->getIncrementalCompileOperation()) + if (!templateGeometry && compile && mSceneManager->getIncrementalCompileOperation()) { mSceneManager->getIncrementalCompileOperation()->add(geometry); } diff --git a/components/terrain/chunkmanager.hpp b/components/terrain/chunkmanager.hpp index 9b7dbf3ee1..9b85e81330 100644 --- a/components/terrain/chunkmanager.hpp +++ b/components/terrain/chunkmanager.hpp @@ -26,6 +26,7 @@ namespace Terrain class CompositeMapRenderer; class Storage; class CompositeMap; + class TerrainDrawable; typedef std::tuple ChunkId; // Center, Lod, Lod Flags @@ -51,7 +52,7 @@ namespace Terrain void releaseGLObjects(osg::State* state) override; private: - osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile); + osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry); osg::ref_ptr createCompositeMapRTT(); diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 2fbd0a7fc0..22f507b3a2 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -166,6 +166,29 @@ namespace mValue->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); } }; + + class UniformCollection + { + public: + static const UniformCollection& value() + { + static UniformCollection instance; + return instance; + } + + osg::ref_ptr mDiffuseMap; + osg::ref_ptr mBlendMap; + osg::ref_ptr mNormalMap; + osg::ref_ptr mColorMode; + + UniformCollection() + : mDiffuseMap(new osg::Uniform("diffuseMap", 0)) + , mBlendMap(new osg::Uniform("blendMap", 1)) + , mNormalMap(new osg::Uniform("normalMap", 2)) + , mColorMode(new osg::Uniform("colorMode", 2)) + { + } + }; } namespace Terrain @@ -176,7 +199,6 @@ namespace Terrain std::vector > passes; unsigned int blendmapIndex = 0; - unsigned int passIndex = 0; for (std::vector::const_iterator it = layers.begin(); it != layers.end(); ++it) { bool firstLayer = (it == layers.begin()); @@ -186,7 +208,7 @@ namespace Terrain if (!blendmaps.empty()) { stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - stateset->setRenderBinDetails(passIndex++, "RenderBin"); + stateset->setRenderBinDetails(firstLayer ? 0 : 1, "RenderBin"); if (!firstLayer) { stateset->setAttributeAndModes(BlendFunc::value(), osg::StateAttribute::ON); @@ -199,32 +221,28 @@ namespace Terrain } } - int texunit = 0; - if (useShaders) { - stateset->setTextureAttributeAndModes(texunit, it->mDiffuseMap); + stateset->setTextureAttributeAndModes(0, it->mDiffuseMap); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("diffuseMap", texunit)); + stateset->addUniform(UniformCollection::value().mDiffuseMap); if (!blendmaps.empty()) { - ++texunit; osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->addUniform(new osg::Uniform("blendMap", texunit)); + stateset->setTextureAttributeAndModes(1, blendmap.get()); + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->addUniform(UniformCollection::value().mBlendMap); } if (it->mNormalMap) { - ++texunit; - stateset->setTextureAttributeAndModes(texunit, it->mNormalMap); - stateset->addUniform(new osg::Uniform("normalMap", texunit)); + stateset->setTextureAttributeAndModes(2, it->mNormalMap); + stateset->addUniform(UniformCollection::value().mNormalMap); } Shader::ShaderManager::DefineMap defineMap; @@ -242,31 +260,27 @@ namespace Terrain } stateset->setAttributeAndModes(shaderManager->getProgram(vertexShader, fragmentShader)); - stateset->addUniform(new osg::Uniform("colorMode", 2)); + stateset->addUniform(UniformCollection::value().mColorMode); } else { // Add the actual layer texture osg::ref_ptr tex = it->mDiffuseMap; - stateset->setTextureAttributeAndModes(texunit, tex.get()); + stateset->setTextureAttributeAndModes(0, tex.get()); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); // Multiply by the alpha map if (!blendmaps.empty()) { osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); + stateset->setTextureAttributeAndModes(1, blendmap.get()); // This is to map corner vertices directly to the center of a blendmap texel. - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->setTextureAttributeAndModes(texunit, TexEnvCombine::value(), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->setTextureAttributeAndModes(1, TexEnvCombine::value(), osg::StateAttribute::ON); } } diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 6a228a75af..7e5cd001ba 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "quadtreenode.hpp" #include "storage.hpp" @@ -54,11 +55,12 @@ namespace Terrain class DefaultLodCallback : public LodCallback { public: - DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid) + DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid, float distanceModifier=0.f) : mFactor(factor) , mMinSize(minSize) , mViewDistance(viewDistance) , mActiveGrid(grid) + , mDistanceModifier(distanceModifier) { } @@ -77,6 +79,8 @@ public: return Deeper; } + dist = std::max(0.f, dist + mDistanceModifier); + if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded return StopTraversal; @@ -91,6 +95,7 @@ private: float mMinSize; float mViewDistance; osg::Vec4i mActiveGrid; + float mDistanceModifier; }; class RootNode : public QuadTreeNode @@ -244,7 +249,27 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize) +class DebugChunkManager : public QuadTreeWorld::ChunkManager +{ +public: + DebugChunkManager(Resource::SceneManager* sceneManager, Storage* storage, unsigned int nodeMask) : mSceneManager(sceneManager), mStorage(storage), mNodeMask(nodeMask) {} + osg::ref_ptr getChunk(float size, const osg::Vec2f& chunkCenter, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + { + osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 }; + auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size, mStorage, mSceneManager, mNodeMask, 5.f, { 1, 0, 0, 0 }); + osg::ref_ptr trans = new osg::MatrixTransform(osg::Matrixf::translate(-center*Constants::CellSizeInUnits)); + trans->setDataVariance(osg::Object::STATIC); + trans->addChild(chunkBorder); + return trans; + } + unsigned int getNodeMask() { return mNodeMask; } +private: + Resource::SceneManager* mSceneManager; + Storage* mStorage; + unsigned int mNodeMask; +}; + +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks) : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) @@ -252,11 +277,18 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour , mVertexLodMod(vertexLodMod) , mViewDistance(std::numeric_limits::max()) , mMinSize(1/8.f) + , mDebugTerrainChunks(debugChunks) { mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapLevel(compMapLevel); mChunkManager->setMaxCompositeGeometrySize(maxCompGeometrySize); mChunkManagers.push_back(mChunkManager.get()); + + if (mDebugTerrainChunks) + { + mDebugChunkManager = std::unique_ptr(new DebugChunkManager(mResourceSystem->getSceneManager(), mStorage, borderMask)); + addChunkManager(mDebugChunkManager.get()); + } } QuadTreeWorld::~QuadTreeWorld() @@ -342,7 +374,7 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f for (QuadTreeWorld::ChunkManager* m : chunkManagers) { - if (m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance*10) + if (m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) continue; osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); if (n) pat->addChild(n); @@ -424,13 +456,13 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) osg::Object * viewer = isCullVisitor ? static_cast(&nv)->getCurrentCamera() : nullptr; bool needsUpdate = true; - ViewData *vd = mViewDataMap->getViewData(viewer, nv.getViewPoint(), mActiveGrid, needsUpdate); - + osg::Vec3f viewPoint = viewer ? nv.getViewPoint() : nv.getEyePoint(); + ViewData *vd = mViewDataMap->getViewData(viewer, viewPoint, mActiveGrid, needsUpdate); if (needsUpdate) { vd->reset(); DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, mActiveGrid); - mRootNode->traverseNodes(vd, nv.getViewPoint(), &lodCallback); + mRootNode->traverseNodes(vd, viewPoint, &lodCallback); } const float cellWorldSize = mStorage->getCellWorldSize(); @@ -491,39 +523,43 @@ View* QuadTreeWorld::createView() void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::Vec4i &grid, std::atomic &abort, Loading::Reporter& reporter) { ensureQuadTreeBuilt(); + const float cellWorldSize = mStorage->getCellWorldSize(); ViewData* vd = static_cast(view); vd->setViewPoint(viewPoint); vd->setActiveGrid(grid); - DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid); - mRootNode->traverseNodes(vd, viewPoint, &lodCallback); - std::size_t progressTotal = 0; - for (unsigned int i = 0, n = vd->getNumEntries(); i < n; ++i) - progressTotal += vd->getEntry(i).mNode->getSize(); - - reporter.addTotal(progressTotal); - - const float cellWorldSize = mStorage->getCellWorldSize(); - for (unsigned int i=0; igetNumEntries() && !abort; ++i) + for (unsigned int pass=0; pass<3; ++pass) { - ViewData::Entry& entry = vd->getEntry(i); + unsigned int startEntry = vd->getNumEntries(); + float distanceModifier=0.f; + if (pass == 1) + distanceModifier = 1024; + else if (pass == 2) + distanceModifier = -1024; + DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid, distanceModifier); + mRootNode->traverseNodes(vd, viewPoint, &lodCallback); + if (pass==0) + { + std::size_t progressTotal = 0; + for (unsigned int i = 0, n = vd->getNumEntries(); i < n; ++i) + progressTotal += vd->getEntry(i).mNode->getSize(); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, mViewDataMap->getReuseDistance()); - reporter.addProgress(entry.mNode->getSize()); - - + reporter.addTotal(progressTotal); + } + const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier)); + for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) + { + ViewData::Entry& entry = vd->getEntry(i); + loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, reuseDistance); + if (pass==0) reporter.addProgress(entry.mNode->getSize()); + entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass + } } - vd->markUnchanged(); -} - -bool QuadTreeWorld::storeView(const View* view, double referenceTime) -{ - return mViewDataMap->storeView(static_cast(view), referenceTime); } void QuadTreeWorld::reportStats(unsigned int frameNumber, osg::Stats *stats) diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 3dd96a0b84..3bd606d6c6 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -5,6 +5,7 @@ #include "terraingrid.hpp" #include +#include namespace osg { @@ -15,12 +16,13 @@ namespace Terrain { class RootNode; class ViewDataMap; + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. class QuadTreeWorld : public TerrainGrid // note: derived from TerrainGrid is only to render default cells (see loadCell) { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks); ~QuadTreeWorld(); @@ -38,7 +40,6 @@ namespace Terrain View* createView() override; void preload(View* view, const osg::Vec3f& eyePoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) override; - bool storeView(const View* view, double referenceTime) override; void rebuildViews() override; void reportStats(unsigned int frameNumber, osg::Stats* stats) override; @@ -72,6 +73,8 @@ namespace Terrain int mVertexLodMod; float mViewDistance; float mMinSize; + bool mDebugTerrainChunks; + std::unique_ptr mDebugChunkManager; }; } diff --git a/components/terrain/terraindrawable.cpp b/components/terrain/terraindrawable.cpp index 746534abb4..231b6f4fed 100644 --- a/components/terrain/terraindrawable.cpp +++ b/components/terrain/terraindrawable.cpp @@ -94,10 +94,10 @@ void TerrainDrawable::cull(osgUtil::CullVisitor *cv) return; } - if (mCompositeMap) + if (mCompositeMap && mCompositeMapRenderer) { mCompositeMapRenderer->setImmediate(mCompositeMap); - mCompositeMap = nullptr; + mCompositeMapRenderer = nullptr; } bool pushedLight = mLightListCallback && mLightListCallback->pushLightState(this, cv); diff --git a/components/terrain/terraindrawable.hpp b/components/terrain/terraindrawable.hpp index dbfdd3c80a..721abe7481 100644 --- a/components/terrain/terraindrawable.hpp +++ b/components/terrain/terraindrawable.hpp @@ -45,6 +45,7 @@ namespace Terrain typedef std::vector > PassVector; void setPasses (const PassVector& passes); + const PassVector& getPasses() const { return mPasses; } void setLightListCallback(SceneUtil::LightListCallback* lightListCallback); @@ -56,6 +57,7 @@ namespace Terrain const osg::BoundingBox& getWaterBoundingBox() const { return mWaterBoundingBox; } void setCompositeMap(CompositeMap* map) { mCompositeMap = map; } + CompositeMap* getCompositeMap() { return mCompositeMap; } void setCompositeMapRenderer(CompositeMapRenderer* renderer) { mCompositeMapRenderer = renderer; } private: diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index e4d043ffc4..3ebc99f1df 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -143,7 +143,7 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo if (!(vd->suitableToUse(activeGrid) && (vd->getViewPoint()-viewPoint).length2() < mReuseDistance*mReuseDistance && vd->getWorldUpdateRevision() >= mWorldUpdateRevision)) { - float shortestDist = std::numeric_limits::max(); + float shortestDist = viewer ? mReuseDistance*mReuseDistance : std::numeric_limits::max(); const ViewData* mostSuitableView = nullptr; for (const ViewData* other : mUsedViews) { @@ -162,26 +162,17 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo vd->copyFrom(*mostSuitableView); return vd; } - } - if (!vd->suitableToUse(activeGrid)) - { - vd->setViewPoint(viewPoint); - vd->setActiveGrid(activeGrid); - needsUpdate = true; + else if (!mostSuitableView) + { + vd->setViewPoint(viewPoint); + vd->setActiveGrid(activeGrid); + vd->setWorldUpdateRevision(mWorldUpdateRevision); + needsUpdate = true; + } } return vd; } -bool ViewDataMap::storeView(const ViewData* view, double referenceTime) -{ - if (view->getWorldUpdateRevision() < mWorldUpdateRevision) - return false; - ViewData* store = createOrReuseView(); - store->copyFrom(*view); - store->setLastUsageTimeStamp(referenceTime); - return true; -} - ViewData *ViewDataMap::createOrReuseView() { ViewData* vd = nullptr; diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 378d07663c..5d814251ea 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -93,7 +93,6 @@ namespace Terrain void clearUnusedViews(double referenceTime); void rebuildViews(); - bool storeView(const ViewData* view, double referenceTime); float getReuseDistance() const { return mReuseDistance; } diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index b62a1cb568..def7a2eccf 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -4,13 +4,14 @@ #include #include #include -#include #include #include #include #include +#include + #include "defs.hpp" #include "cellborder.hpp" @@ -45,7 +46,7 @@ namespace Terrain class ChunkManager; class CompositeMapRenderer; - class HeightCullCallback : public osg::NodeCallback + class HeightCullCallback : public SceneUtil::NodeCallback { public: void setLowZ(float z) @@ -75,7 +76,7 @@ namespace Terrain return mMask; } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (mLowZ <= mHighZ) traverse(node, nv); @@ -155,10 +156,6 @@ namespace Terrain virtual void preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) {} - /// Store a preloaded view into the cache with the intent that the next rendering traversal can use it. - /// @note Not thread safe. - virtual bool storeView(const View* view, double referenceTime) {return true;} - virtual void rebuildViews() {} virtual void reportStats(unsigned int frameNumber, osg::Stats* stats) {} diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index 045fe3cf5c..faebc782aa 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -86,14 +86,11 @@ namespace VFS return mIndex.find(normalized) != mIndex.end(); } - const std::map& Manager::getIndex() const + std::string Manager::normalizeFilename(const std::string& name) const { - return mIndex; - } - - void Manager::normalizeFilename(std::string &name) const - { - normalize_path(name, mStrict); + std::string result = name; + normalize_path(result, mStrict); + return result; } std::string Manager::getArchive(const std::string& name) const @@ -107,4 +104,24 @@ namespace VFS } return {}; } + + namespace + { + bool startsWith(std::string_view text, std::string_view start) + { + return text.rfind(start, 0) == 0; + } + } + + Manager::RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(const std::string& path) const + { + if (path.empty()) + return { mIndex.begin(), mIndex.end() }; + auto normalized = normalizeFilename(path); + const auto it = mIndex.lower_bound(normalized); + if (it == mIndex.end() || !startsWith(it->first, normalized)) + return { it, it }; + ++normalized.back(); + return { it, mIndex.lower_bound(normalized) }; + } } diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index 5a09a995eb..8568e8e784 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -12,6 +12,19 @@ namespace VFS class Archive; class File; + template + class IteratorPair + { + public: + IteratorPair(Iterator first, Iterator last) : mFirst(first), mLast(last) {} + Iterator begin() const { return mFirst; } + Iterator end() const { return mLast; } + + private: + Iterator mFirst; + Iterator mLast; + }; + /// @brief The main class responsible for loading files from a virtual file system. /// @par Various archive types (e.g. directories on the filesystem, or compressed archives) /// can be registered, and will be merged into a single file tree. If the same filename is @@ -19,6 +32,21 @@ namespace VFS /// @par Most of the methods in this class are considered thread-safe, see each method documentation for details. class Manager { + class RecursiveDirectoryIterator + { + public: + RecursiveDirectoryIterator(std::map::const_iterator it) : mIt(it) {} + const std::string& operator*() const { return mIt->first; } + const std::string* operator->() const { return &mIt->first; } + bool operator!=(const RecursiveDirectoryIterator& other) { return mIt != other.mIt; } + RecursiveDirectoryIterator& operator++() { ++mIt; return *this; } + + private: + std::map::const_iterator mIt; + }; + + using RecursiveDirectoryRange = IteratorPair; + public: /// @param strict Use strict path handling? If enabled, no case folding will /// be done, but slash/backslash conversions are always done. @@ -40,13 +68,9 @@ namespace VFS /// @note May be called from any thread once the index has been built. bool exists(const std::string& name) const; - /// Get a complete list of files from all archives - /// @note May be called from any thread once the index has been built. - const std::map& getIndex() const; - /// Normalize the given filename, making slashes/backslashes consistent, and lower-casing if mStrict is false. /// @note May be called from any thread once the index has been built. - void normalizeFilename(std::string& name) const; + [[nodiscard]] std::string normalizeFilename(const std::string& name) const; /// Retrieve a file by name. /// @note Throws an exception if the file can not be found. @@ -59,6 +83,13 @@ namespace VFS Files::IStreamPtr getNormalized(const std::string& normalizedName) const; std::string getArchive(const std::string& name) const; + + /// Recursivly iterate over the elements of the given path + /// In practice it return all files of the VFS starting with the given path + /// @note the path is normalized + /// @note May be called from any thread once the index has been built. + RecursiveDirectoryRange getRecursiveDirectoryIterator(const std::string& path) const; + private: bool mStrict; diff --git a/docs/source/reference/modding/paths.rst b/docs/source/reference/modding/paths.rst index bc955b703d..db5000527b 100644 --- a/docs/source/reference/modding/paths.rst +++ b/docs/source/reference/modding/paths.rst @@ -29,7 +29,7 @@ Savegames +--------------+-----------------------------------------------------------------------------------------------------+ | OS | Location | +==============+=====================================================================================================+ -| Linux | ``$HOME/.local/share/openmw/saves`` | +| Linux | ``$HOME/.local/share/openmw/saves`` | +--------------+-----------------------------------------------------------------------------------------------------+ | Mac | ``$HOME/Library/Application\ Support/openmw/saves`` | +--------------+---------------+-------------------------------------------------------------------------------------+ diff --git a/docs/source/reference/modding/settings/terrain.rst b/docs/source/reference/modding/settings/terrain.rst index e6018a8655..752260fd48 100644 --- a/docs/source/reference/modding/settings/terrain.rst +++ b/docs/source/reference/modding/settings/terrain.rst @@ -100,6 +100,18 @@ max composite geometry size Controls the maximum size of simple composite geometry chunk in cell units. With small values there will more draw calls and small textures, but higher values create more overdraw (not every texture layer is used everywhere). +debug chunks +------------ + +:Type: boolean +:Range: True/False +:Default: False + +This debug setting allows you to see the borders of each chunks of the world by drawing lines arround them (as with toggleborder). +If object paging is set to true then this debug setting will allows you to see what objects have been merged in the scene +by making them colored randomly. + + object paging ------------- @@ -194,12 +206,3 @@ object paging min size cost multiplier This setting adjusts the calculated cost of merging an object used in the mentioned functionality. The larger this value is, the less expensive objects can be before they are discarded. See the formula above to figure out the math. - -object paging debug batches ---------------------------- -:Type: boolean -:Range: True/False -:Default: False - -This debug setting allows you to see what objects have been merged in the scene -by making them colored randomly. diff --git a/files/lua_api/openmw/async.lua b/files/lua_api/openmw/async.lua index 68f63b5193..cc3a233f8a 100644 --- a/files/lua_api/openmw/async.lua +++ b/files/lua_api/openmw/async.lua @@ -48,5 +48,12 @@ -- @param #number delay -- @param #function func +------------------------------------------------------------------------------- +-- Wraps Lua function with `Callback` object that can be used in async API calls. +-- @function [parent=#async] callback +-- @param self +-- @param #function func +-- @return #Callback + return nil diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index c24d5e3d10..f1aeb07b24 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -32,5 +32,47 @@ -- @param openmw.query#Query query -- @return openmw.core#ObjectList +------------------------------------------------------------------------------- +-- @type COLLISION_TYPE +-- @field [parent=#COLLISION_TYPE] #number World +-- @field [parent=#COLLISION_TYPE] #number Door +-- @field [parent=#COLLISION_TYPE] #number Actor +-- @field [parent=#COLLISION_TYPE] #number HeightMap +-- @field [parent=#COLLISION_TYPE] #number Projectile +-- @field [parent=#COLLISION_TYPE] #number Water +-- @field [parent=#COLLISION_TYPE] #number Default Used by deafult: World+Door+Actor+HeightMap + +------------------------------------------------------------------------------- +-- Collision types that are used in `castRay`. +-- Several types can be combined with '+'. +-- @field [parent=#nearby] #COLLISION_TYPE COLLISION_TYPE + +------------------------------------------------------------------------------- +-- Result of raycasing +-- @type RayCastingResult +-- @field [parent=#RayCastingResult] #boolean hit Is there a collision? (true/false) +-- @field [parent=#RayCastingResult] openmw.util#Vector3 hitPos Position of the collision point (nil if no collision) +-- @field [parent=#RayCastingResult] openmw.util#Vector3 hitNormal Normal to the surface in the collision point (nil if no collision) +-- @field [parent=#RayCastingResult] openmw.core#GameObject hitObject The object the ray has collided with (can be nil) + +------------------------------------------------------------------------------- +-- Cast ray from one point to another and return the first collision. +-- @function [parent=#nearby] castRay +-- @param openmw.util#Vector3 from Start point of the ray. +-- @param openmw.util#Vector3 to End point of the ray. +-- @param #table options An optional table with additional optional arguments. Can contain: +-- `ignore` - an object to ignore (specify here the source of the ray); +-- `collisionType` - object types to work with (see @{openmw.nearby#COLLISION_TYPE}), several types can be combined with '+'; +-- `radius` - the radius of the ray (zero by default). If not zero then castRay actually casts a sphere with given radius. +-- NOTE: currently `ignore` is not supported if `radius>0`. +-- @return #RayCastingResult +-- @usage if nearby.castRay(pointA, pointB).hit then print('obstacle between A and B') end +-- @usage local res = nearby.castRay(self.position, enemy.position, {ignore=self}) +-- if res.hitObject and res.hitObject ~= enemy then obstacle = res.hitObject end +-- @usage local res = nearby.castRay(self.position, targetPos, { +-- collisionType=nearby.COLLISION_TYPE.HeightMap + nearby.COLLISION_TYPE.Water, +-- radius = 10, +-- }) + return nil diff --git a/files/lua_api/openmw/util.lua b/files/lua_api/openmw/util.lua index 7d31ced408..84e640ff40 100644 --- a/files/lua_api/openmw/util.lua +++ b/files/lua_api/openmw/util.lua @@ -6,7 +6,7 @@ ------------------------------------------------------------------------------- --- Limits given value to the interval [`from`, `to`] +-- Limits given value to the interval [`from`, `to`]. -- @function [parent=#util] clamp -- @param #number value -- @param #number from @@ -14,7 +14,7 @@ -- @return #number min(max(value, from), to) ------------------------------------------------------------------------------- --- Adds `2pi*k` and puts the angle in range `[-pi, pi]` +-- Adds `2pi*k` and puts the angle in range `[-pi, pi]`. -- @function [parent=#util] normalizeAngle -- @param #number angle Angle in radians -- @return #number Angle in range `[-pi, pi]` @@ -48,13 +48,13 @@ -- @return #Vector2. ------------------------------------------------------------------------------- --- Length of the vector +-- Length of the vector. -- @function [parent=#Vector2] length -- @param self -- @return #number ------------------------------------------------------------------------------- --- Square of the length of the vector +-- Square of the length of the vector. -- @function [parent=#Vector2] length2 -- @param self -- @return #number @@ -146,5 +146,84 @@ -- @param #Vector3 v -- @return #Vector3 +------------------------------------------------------------------------------- +-- @type Transform + +------------------------------------------------------------------------------- +-- Returns the inverse transform. +-- @function [parent=#Transform] inverse +-- @param self +-- @return #Transform. + +------------------------------------------------------------------------------- +-- @type TRANSFORM +-- @field [parent=#TRANSFORM] #Transform identity Empty transform. + +------------------------------------------------------------------------------- +-- Movement by given vector. +-- @function [parent=#TRANSFORM] move +-- @param #Vector3 offset. +-- @return #Transform. +-- @usage +-- -- Accepts either 3 numbers or a 3D vector +-- util.transform.move(x, y, z) +-- util.transform.move(util.vector3(x, y, z)) + +------------------------------------------------------------------------------- +-- Scale transform. +-- @function [parent=#TRANSFORM] scale +-- @param #number scaleX. +-- @param #number scaleY. +-- @param #number scaleZ. +-- @return #Transform. +-- @usage +-- -- Accepts either 3 numbers or a 3D vector +-- util.transform.scale(x, y, z) +-- util.transform.scale(util.vector3(x, y, z)) + + +------------------------------------------------------------------------------- +-- Rotation (any axis). +-- @function [parent=#TRANSFORM] rotate +-- @param #number angle +-- @param #Vector3 axis. +-- @return #Transform. + +------------------------------------------------------------------------------- +-- X-axis rotation. +-- @function [parent=#TRANSFORM] rotateX +-- @param #number angle +-- @return #Transform. + +------------------------------------------------------------------------------- +-- Y-axis rotation. +-- @function [parent=#TRANSFORM] rotateY +-- @param #number angle +-- @return #Transform. + +------------------------------------------------------------------------------- +-- Z-axis rotation. +-- @function [parent=#TRANSFORM] rotateZ +-- @param #number angle +-- @return #Transform. + +------------------------------------------------------------------------------- +-- 3D transforms (scale/move/rotate) that can be applied to 3D vectors. +-- Several transforms can be combined and applied to a vector using multiplication. +-- Combined transforms apply in reverse order (from right to left). +-- @field [parent=#util] #TRANSFORM transform +-- @usage +-- local util = require('openmw.util') +-- local trans = util.transform +-- local fromActorSpace = trans.move(actor.position) * trans.rotateZ(actor.rotation.z) +-- +-- -- rotation is applied first, movement is second +-- local posBehindActor = fromActorSpace * util.vector3(0, -100, 0) +-- +-- -- equivalent to trans.rotateZ(-actor.rotation.z) * trans.move(-actor.position) +-- local toActorSpace = fromActorSpace:inverse() +-- local relativeTargetPos = toActorSpace * target.position +-- local deltaAngle = math.atan2(relativeTargetPos.y, relativeTargetPos.x) + return nil diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 7f57ba529d..5ed8109465 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1,15 +1,7 @@ -# WARNING: If this file is named settings-default.cfg or was generated -# from defaults.bin, then editing this file might have no effect, as -# these settings may be overwritten by your user settings.cfg file -# (see documentation for its location). +# WARNING: Users should NOT edit this file. Users should add their personal preferences to the settings.cfg file overriding this file. +# For the location of the settings.cfg file, as well as more detailed settings documentation, refer to: # -# This file provides minimal documentation for each setting, and -# ranges of recommended values. For detailed explanations of the -# significance of each setting, interaction with other settings, hard -# limits on value ranges and more information in general, please read -# the detailed documentation at: -# -# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html +# https://openmw.readthedocs.io/en/latest/reference/modding/settings/index.html # [Camera] @@ -139,6 +131,9 @@ composite map resolution = 512 # Controls the maximum size of composite geometry, should be >= 1.0. With low values there will be many small chunks, with high values - lesser count of bigger chunks. max composite geometry size = 4.0 +# Draw lines arround chunks. +debug chunks = false + # Use object paging for non active cells object paging = true diff --git a/files/shaders/nv_default_fragment.glsl b/files/shaders/nv_default_fragment.glsl index 1bd0f4e310..245f83b620 100644 --- a/files/shaders/nv_default_fragment.glsl +++ b/files/shaders/nv_default_fragment.glsl @@ -1,4 +1,5 @@ #version 120 +#pragma import_defines(FORCE_OPAQUE) #if @useUBO #extension GL_ARB_uniform_buffer_object : require @@ -24,8 +25,6 @@ varying vec2 normalMapUV; varying vec4 passTangent; #endif -uniform bool noAlpha; - varying float euclideanDepth; varying float linearDepth; @@ -95,9 +94,9 @@ void main() #endif gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); -#if @translucentFramebuffer - if (noAlpha) - gl_FragData[0].a = 1.0; +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE + // having testing & blending isn't enough - we need to write an opaque pixel to be opaque + gl_FragData[0].a = 1.0; #endif applyShadowDebugOverlay(); diff --git a/files/shaders/nv_nolighting_fragment.glsl b/files/shaders/nv_nolighting_fragment.glsl index 27679a069f..7c4f4737e0 100644 --- a/files/shaders/nv_nolighting_fragment.glsl +++ b/files/shaders/nv_nolighting_fragment.glsl @@ -1,4 +1,5 @@ #version 120 +#pragma import_defines(FORCE_OPAQUE) #if @useGPUShader4 #extension GL_EXT_gpu_shader4: require @@ -9,8 +10,6 @@ uniform sampler2D diffuseMap; varying vec2 diffuseMapUV; #endif -uniform bool noAlpha; - #if @radialFog varying float euclideanDepth; #else @@ -46,9 +45,8 @@ void main() float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); #endif -#if @translucentFramebuffer - if (noAlpha) - gl_FragData[0].a = 1.0; +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE + gl_FragData[0].a = 1.0; #endif gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index d304b2abe8..6f6cede4e4 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -1,4 +1,5 @@ #version 120 +#pragma import_defines(FORCE_OPAQUE) #if @useUBO #extension GL_ARB_uniform_buffer_object : require @@ -58,7 +59,6 @@ uniform mat2 bumpMapMatrix; #endif uniform bool simpleWater; -uniform bool noAlpha; varying float euclideanDepth; varying float linearDepth; @@ -220,10 +220,9 @@ void main() #endif gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); -#if @translucentFramebuffer +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE // having testing & blending isn't enough - we need to write an opaque pixel to be opaque - if (noAlpha) - gl_FragData[0].a = 1.0; + gl_FragData[0].a = 1.0; #endif applyShadowDebugOverlay(); diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py index f0e6ce4010..037aafea00 100755 --- a/scripts/osg_stats.py +++ b/scripts/osg_stats.py @@ -38,27 +38,41 @@ import termtables @click.option('--timeseries_sum', is_flag=True, help='Add a graph to timeseries for a sum per frame of all given timeseries metrics.') @click.option('--commulative_timeseries_sum', is_flag=True, - help='Add a graph to timeseries for a sum per frame of all given commulative timeseries.') + help='Add a graph to timeseries for a sum per frame of all given commulative timeseries.') @click.option('--stats_sum', is_flag=True, help='Add a row to stats table for a sum per frame of all given stats metrics.') @click.option('--begin_frame', type=int, default=0, help='Start processing from this frame.') @click.option('--end_frame', type=int, default=sys.maxsize, help='End processing at this frame.') +@click.option('--frame_number_name', type=str, default='FrameNumber', + help='Frame number metric name.') +@click.option('--hist_threshold', type=str, multiple=True, + help='Show a histogram for given metric only for frames with threshold_name metric over threshold_value.') +@click.option('--threshold_name', type=str, default='Frame duration', + help='Frame duration metric name.') +@click.option('--threshold_value', type=float, default=1.05/60, + help='Threshold for hist_over.') @click.argument('path', type=click.Path(), nargs=-1) def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats, timeseries_sum, stats_sum, begin_frame, end_frame, path, - commulative_timeseries, commulative_timeseries_sum): + commulative_timeseries, commulative_timeseries_sum, frame_number_name, + hist_threshold, threshold_name, threshold_value): sources = {v: list(read_data(v)) for v in path} if path else {'stdin': list(read_data(None))} keys = collect_unique_keys(sources) - frames = collect_per_frame(sources=sources, keys=keys, begin_frame=begin_frame, end_frame=end_frame) + frames, begin_frame, end_frame = collect_per_frame( + sources=sources, keys=keys, begin_frame=begin_frame, + end_frame=end_frame, frame_number_name=frame_number_name, + ) if print_keys: for v in keys: print(v) if timeseries: - draw_timeseries(sources=frames, keys=timeseries, add_sum=timeseries_sum) + draw_timeseries(sources=frames, keys=timeseries, add_sum=timeseries_sum, + begin_frame=begin_frame, end_frame=end_frame) if commulative_timeseries: - draw_commulative_timeseries(sources=frames, keys=commulative_timeseries, add_sum=commulative_timeseries_sum) + draw_commulative_timeseries(sources=frames, keys=commulative_timeseries, add_sum=commulative_timeseries_sum, + begin_frame=begin_frame, end_frame=end_frame) if hist: draw_hists(sources=frames, keys=hist) if hist_ratio: @@ -69,6 +83,9 @@ def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats, draw_plots(sources=frames, plots=plot) if stats: print_stats(sources=frames, keys=stats, stats_sum=stats_sum) + if hist_threshold: + draw_hist_threshold(sources=frames, keys=hist_threshold, begin_frame=begin_frame, + threshold_name=threshold_name, threshold_value=threshold_value) matplotlib.pyplot.show() @@ -92,19 +109,26 @@ def read_data(path): frame[key] = to_number(value) -def collect_per_frame(sources, keys, begin_frame, end_frame): +def collect_per_frame(sources, keys, begin_frame, end_frame, frame_number_name): + assert begin_frame < end_frame result = collections.defaultdict(lambda: collections.defaultdict(list)) + begin_frame = max(begin_frame, min(v[0][frame_number_name] for v in sources.values())) + end_frame = min(end_frame, begin_frame + max(len(v) for v in sources.values())) + for name in sources.keys(): + for key in keys: + result[name][key] = [0] * (end_frame - begin_frame) for name, frames in sources.items(): for frame in frames: - for key in keys: - if key in frame: - result[name][key].append(frame[key]) - else: - result[name][key].append(None) - for name, sources in result.items(): - for key, values in sources.items(): - result[name][key] = numpy.array(values[begin_frame:end_frame]) - return result + number = frame[frame_number_name] + if begin_frame <= number < end_frame: + index = number - begin_frame + for key in keys: + if key in frame: + result[name][key][index] = frame[key] + for name in result.keys(): + for key in keys: + result[name][key] = numpy.array(result[name][key]) + return result, begin_frame, end_frame def collect_unique_keys(sources): @@ -116,12 +140,11 @@ def collect_unique_keys(sources): return sorted(result) -def draw_timeseries(sources, keys, add_sum): +def draw_timeseries(sources, keys, add_sum, begin_frame, end_frame): fig, ax = matplotlib.pyplot.subplots() + x = numpy.array(range(begin_frame, end_frame)) for name, frames in sources.items(): - x = numpy.array(range(max(len(v) for k, v in frames.items() if k in keys))) for key in keys: - print(key, name) ax.plot(x, frames[key], label=f'{key}:{name}') if add_sum: ax.plot(x, numpy.sum(list(frames[k] for k in keys), axis=0), label=f'sum:{name}') @@ -130,10 +153,10 @@ def draw_timeseries(sources, keys, add_sum): fig.canvas.set_window_title('timeseries') -def draw_commulative_timeseries(sources, keys, add_sum): +def draw_commulative_timeseries(sources, keys, add_sum, begin_frame, end_frame): fig, ax = matplotlib.pyplot.subplots() + x = numpy.array(range(begin_frame, end_frame)) for name, frames in sources.items(): - x = numpy.array(range(max(len(v) for k, v in frames.items() if k in keys))) for key in keys: ax.plot(x, numpy.cumsum(frames[key]), label=f'{key}:{name}') if add_sum: @@ -227,7 +250,6 @@ def print_stats(sources, keys, stats_sum): if stats_sum: stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys))) metrics = list(stats[0].keys()) - max_key_size = max(len(tuple(v.values())[0]) for v in stats) termtables.print( [list(v.values()) for v in stats], header=metrics, @@ -235,6 +257,27 @@ def print_stats(sources, keys, stats_sum): ) +def draw_hist_threshold(sources, keys, begin_frame, threshold_name, threshold_value): + for name, frames in sources.items(): + indices = [n for n, v in enumerate(frames[threshold_name]) if v > threshold_value] + numbers = [v + begin_frame for v in indices] + x = [v for v in range(0, len(indices))] + fig, ax = matplotlib.pyplot.subplots() + ax.set_title(f'Frames with "{threshold_name}" > {threshold_value} ({len(indices)})') + ax.bar(x, [frames[threshold_name][v] for v in indices], label=threshold_name, color='black', alpha=0.2) + prev = 0 + for key in keys: + values = [frames[key][v] for v in indices] + ax.bar(x, values, bottom=prev, label=key) + prev = values + ax.hlines(threshold_value, x[0] - 1, x[-1] + 1, color='black', label='threshold', linestyles='dashed') + ax.xaxis.set_major_locator(matplotlib.pyplot.FixedLocator(x)) + ax.xaxis.set_major_formatter(matplotlib.pyplot.FixedFormatter(numbers)) + ax.grid(True) + ax.legend() + fig.canvas.set_window_title(f'hist_threshold:{name}') + + def filter_not_none(values): return [v for v in values if v is not None] @@ -269,5 +312,6 @@ def to_number(value): except ValueError: return float(value) + if __name__ == '__main__': main()